上篇說了多執行緒的概述,這篇說說具體實現。
muduo的多線程是由線程池中啟動的。線程池類EventLoopThreadPool在TcpServer類中建立一個心得執行個體。發現在muduo中,各種類的關係基本上引用和包含即組合關係,很少有派生關係的,沒有繼承關係就沒有虛函數的應用了。
可能陳碩覺得繼承關係比較複雜,耦合度太高,破壞整體設計。但是我覺得muduo中那麼多不同種類的智能指標,還有基於boost或std的函數綁定,本身就夠複雜的了。所以我打算有時間用c語言來改寫一下muduo,把那些智能指標,函數綁定等弱化,只關注網路架構本身。不過說實話,muduo本身就是基於C++的,用智能指標和函數綁定很正常,而且人家還用得非常到位。所以要想往C++方面發展,還是得精通上面的知識。我本身是搞C++,而且搞了很久,而且決定一直搞下去。不過在接觸c項目,指令碼語言,還有現代網路並發語言golang之後,還是覺得用C++寫項目開發效率太低了,而且很難駕馭,指標就是一個雷區。精通C++的時間與其收穫性價比是很低的,所以我決定以後不再畫太多的時間在C++上,對於C++項目,我只是關注其架構本身。
好了廢話少說。EventLoopThreadPool的start在TcpServer的start中調用,他會建立n個線程並啟動,n是EventLoopThreadPool的成員變數,可以配置,也可以是0個。每個線程是EventLoopThread的執行個體。而EventLoopThread有個組合對象Thread,他才是線程建立和啟動真正的地方。代碼如下:
void TcpServer::start(){ if (started_.getAndSet(1) == 0) { threadPool_->start(threadInitCallback_); assert(!acceptor_->listenning()); loop_->runInLoop( std::bind(&Acceptor::listen, get_pointer(acceptor_))); }}
void EventLoopThreadPool::start(const ThreadInitCallback& cb){ assert(!started_); baseLoop_->assertInLoopThread(); started_ = true; for (int i = 0; i < numThreads_; ++i) { char buf[name_.size() + 32]; snprintf(buf, sizeof buf, "%s%d", name_.c_str(), i); EventLoopThread* t = new EventLoopThread(cb, buf); threads_.push_back(std::unique_ptr<EventLoopThread>(t)); loops_.push_back(t->startLoop()); } if (numThreads_ == 0 && cb) { cb(baseLoop_); }}
void Thread::start(){ assert(!started_); started_ = true; // FIXME: move(func_) detail::ThreadData* data = new detail::ThreadData(func_, name_, &tid_, &latch_); if (pthread_create(&pthreadId_, NULL, &detail::startThread, data)) { started_ = false; delete data; // or no delete? LOG_SYSFATAL << "Failed in pthread_create"; } else { latch_.wait(); assert(tid_ > 0); }}
Thread的線程函數已經綁定在了EventLoopThread::threadFunc裡,這個是在EventLoopThread建構函式是初始化的。代碼如下:
void EventLoopThread::threadFunc(){ EventLoop loop; if (callback_) { callback_(&loop); } { MutexLockGuard lock(mutex_); loop_ = &loop; cond_.notify(); } loop.loop(); //assert(exiting_); loop_ = NULL;}
這裡有個問題是,主線程會加入新線程的loop執行個體,而這個loop執行個體又是線上程的線程函數裡建立的。所以很顯然主線程和新線程有個同步的過程,並且多個新線程之間有臨界區的問題。這些是用條件變數pthread_cond_t和互斥變數mutext來實現的。
新線程線程函數在棧上申明一個EventLoop對象之後,便通知主線程了。這使得主線程返回,而新線程函數中loop開始運作迴圈了。那麼主線程又是如何跨線程調用新線程的函數的呢?這個下篇再說。