轉載:gRPC 的 C++ 動態線程池源碼分析

轉載: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中(在適當的時機,線程池會deletedead_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亂來喚起一個線程來運行函數,如果線程都在運行函數,就創建新的線程來運行函數。直到程序運行完後,查看隊列中是否還有需要運行的函數,有就進行運行新的函數,沒有的話就檢查線程數目,線程數目如果不超過預設置的線程數,該線程就繼續掛起,如果超過了就釋放。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章