轉載:http://senlinzhan.github.io/2017/09/09/grpc-dynamic-thread-pool/
作者:Senlin
自學記錄用,侵刪,建議到原博客網址學習。
固定線程池
提到線程池,通常說的都是固定大小的線程池,固定線程池的原理是這樣的:
- 線程池由一個線程安全的隊列,以及多個 worker 線程組成。
- 可以有多個 producer 線程,它們負責提交任務給線程池。
- 接收到新任務之後,線程池會喚醒某個 worker 線程,worker 線程醒來後會取出任務並執行。
雖然固定線程池實現起來很簡單,但卻有着幾個缺陷:
- 無法動態擴容:worker 線程的個數是固定的,不能隨着任務數的增長而增長。
- 無法動態縮容:如果有很多 worker 線程處於空閒狀態,就會造成資源的浪費。
動態線程池
對於線程池,我們希望它可以動態增長,這樣纔不會造成任務隊列的堆積,另一反面,也希望它能適當回收一些空閒的線程,以節省系統資源。
線程池接口
gRPC 內部使用了動態線程池,它的接口長這樣:
class DynamicThreadPool
{
public:
DynamicThreadPool(int reserve_threads);
~DynamicThreadPool();
void Add(const std::function<void()> &callback); // 提交任務
};
那麼DynamicThreadPool
是怎樣管理線程的呢?
DynamicThreadPool
並不會限制線程的數量,理論上這意味着線程數量可以無限增長。- 構造函數接收一個參數
reserve_threads
,這個參數與線程池的縮容策略有關:它表示線程池最多只能有reserve_threads
個空閒線程。也就是說,如果線程池的空閒線程數量多於這個值,那麼多出來的那些線程就會被系統回收。
線程池的構造
首先看看DynamicThreadPool
的數據成員:
class DynamicThreadPool
{
private:
std::mutex mu_; // 互斥鎖,保護數據成員
std::condition_variable cv_; // 條件變量
std::condition_variable shutdown_cv_; // 條件變量,與線程池析構相關
bool shutdown_; // 線程池是否即將析構
std::queue<std::function<void()>> callbacks_; // 任務隊列
int reserve_threads_; // 最大空閒線程數
int nthreads_; // 當前線程數
int threads_waiting_; // 空閒線程數
std::list<DynamicThread*> dead_threads_; // 保存已經終止的線程
};
DynamicThreadPool
的構造函數會先創建reserve_threads
個線程:
DynamicThreadPool::DynamicThreadPool(int reserve_threads)
: shutdown_(false),
reserve_threads_(reserve_threads),
nthreads_(0),
threads_waiting_(0)
{
for (int i = 0; i < reserve_threads_; i++)
{
std::lock_guard<std::mutex> lock(mu_);
nthreads_++;
new DynamicThread(this); // 創建新線程
}
}
可以看到,線程池不會直接使用std::thread
,而是使用自己封裝的DynamicThread
:
class DynamicThreadPool
{
private:
class DynamicThread
{
public:
DynamicThread(DynamicThreadPool* pool);
~DynamicThread();
private:
DynamicThreadPool* pool_;
std::unique_ptr<std::thread> thd_;
void ThreadFunc();
};
}
與std::thread
相比,DynamicThread
遵循了 RAII 的原則:DynamicThread
在析構時,會調用線程的join()
函數,用來確保正確地釋放線程資源:
DynamicThreadPool::DynamicThread::DynamicThread(DynamicThreadPool *pool)
: pool_(pool),
thd_(new std::thread(&DynamicThreadPool::DynamicThread::ThreadFunc, this))
{
}
DynamicThreadPool::DynamicThread::~DynamicThread()
{
thd_->join();
thd_.reset();
}
線程池的析構
DynamicThread
在退出之前,會將自己添加到線程池的dead_threads
中(在適當的時機,線程池會delete
掉dead_threads
中的所有線程,保證資源的釋放)。
void DynamicThreadPool::DynamicThread::ThreadFunc()
{
// 執行工作
pool_->ThreadFunc();
// 執行完工作,這時 std::thread 將要退出
std::unique_lock<std::mutex> lock(pool_->mu_);
pool_->nthreads_--; // 當前線程數減 1
// 將自己添加到 dead_threads 中
pool_->dead_threads_.push_back(this);
// 如果線程池正在析構 (休眠等待所有線程退出)
// 並且所有線程均已退出了,那就喚醒線程池
if ((pool_->shutdown_) && (pool_->nthreads_ == 0))
{
pool_->shutdown_cv_.notify_one();
}
}
線程池的析構函數首先會喚醒所有休眠的線程,然後等待所有線程都退出,之後再調用reapThreads()
清理掉所有線程:
DynamicThreadPool::~DynamicThreadPool()
{
std::unique_lock<std::mutex> lock_(mu_);
shutdown_ = true;
// 喚醒所有線程
cv_.notify_all();
// 等待所有線程都退出
while (nthreads_ != 0)
{
shutdown_cv_.wait(lock_);
}
// 清理掉所有終止的線程
ReapThreads(&dead_threads_);
}
void DynamicThreadPool::ReapThreads(std::list<DynamicThread*> *tlist)
{
for (auto t = tlist->begin(); t != tlist->end(); t = tlist->erase(t))
{
delete *t;
}
}
任務的提交與執行
線程池的Add()
函數用來提交任務,任務會被放到任務隊列中,並喚醒一個空閒的線程去處理任務:
void DynamicThreadPool::Add(const std::function<void ()> &callback)
{
std::lock_guard<std::mutex> lock(mu_);
// 將任務添加到任務隊列中
callbacks_.push(callback);
// 如果沒有空閒的線程,就創建新的線程
if (threads_waiting_ == 0)
{
nthreads_++;
new DynamicThread(this);
}
else
{
// 喚醒一個空閒的線程
cv_.notify_one();
}
// 釋放掉已經終止的線程
if (!dead_threads_.empty())
{
ReapThreads(&dead_threads_);
}
}
而ThreadFunc()
則展示了線程消費任務的邏輯:
void DynamicThreadPool::ThreadFunc()
{
for (;;)
{
std::unique_lock<std::mutex> lock(mu_);
// 如果任務隊列爲空,那就讓自己休眠
if (!shutdown_ && callbacks_.empty())
{
// 如果已經有足夠多的空閒線程,那麼就退出自己
if (threads_waiting_ >= reserve_threads_)
{
break;
}
threads_waiting_++;
cv_.wait(lock); // 進入休眠
threads_waiting_--;
}
// 判斷 shutdown 之前需要保證所有任務都被執行完
if (!callbacks_.empty())
{
auto cb = callbacks_.front();
callbacks_.pop();
lock.unlock();
cb();
}
else if (shutdown_)
{
break;
}
}
}
完整的代碼可以參照這裏:https://github.com/senlinzhan/code-for-blog/tree/master/grpc_dynamic_thread_pool
個人理解:
首先,創建動態線程池,並且預先創建n個線程掛起。然後使用add函數將需要運行的函數分配線程。通過condition_variable cv亂來喚起一個線程來運行函數,如果線程都在運行函數,就創建新的線程來運行函數。直到程序運行完後,查看隊列中是否還有需要運行的函數,有就進行運行新的函數,沒有的話就檢查線程數目,線程數目如果不超過預設置的線程數,該線程就繼續掛起,如果超過了就釋放。