簡單的執行緒池(二)

cnblogs發表於2021-11-30

概要

作者在簡單的執行緒池中採用了非阻塞的(nonblocking)執行緒同步方式,在此文中作者將採用阻塞的(blocking)執行緒同步方式實現相同特性的執行緒池。

本文中不再贅述與簡單的執行緒池相同的內容。如有不明之處,請參考該部落格

實現

以下程式碼給出了此執行緒池的實現。

class Thread_Pool {

  private:

    struct Task_Wrapper { ...

    };

    atomic<bool> _done_;
    Blocking_Queue<Task_Wrapper> _queue_;            // #1
    unsigned _workersize_;
    thread* _workers_;

    void work() {
        Task_Wrapper task;
        while (!_done_.load(memory_order_acquire)) {
            _queue_.pop(task);                         // #3
            task();
        }
    }

    void stop() {
        while (!_queue_.empty())                // #2
            std::this_thread::yield();
        _done_.store(true, memory_order_release);
        for (unsigned i = 0; i < _workersize_; ++i)        // #5
            _queue_.push([] {});
        for (unsigned i = 0; i < _workersize_; ++i) {
            if (_workers_[i].joinable())
                _workers_[i].join();                    // #4
        }
        delete[] _workers_;
    }

  public:
    Thread_Pool() : _done_(false) {
        try {
            _workersize_ = thread::hardware_concurrency();
            _workers_ = new thread[_workersize_];
            for (unsigned i = 0; i < _workersize_; ++i) {
                _workers_[i] = thread(&Thread_Pool::work, this);
            }
        } catch (...) {
            stop();
            throw;
        }
    }
    ~Thread_Pool() {
        stop();
    }

    template<class Callable>
    future<typename std::result_of<Callable()>::type> submit(Callable c) {
        typedef typename std::result_of<Callable()>::type R;
        packaged_task<R()> task(c);
        future<R> r = task.get_future();
        _queue_.push(std::move(task));
        return r;
    }

};

Task_Wrapper 具體化了執行緒安全的任務佇列 Blocking_Queue<>(#1)。稍後對 Blocking_Queue<> 做說明。

終止執行緒池時,任務佇列中的剩餘任務被執行(#2)完後,任務佇列處於被清空的狀態。在 _done_ 還未被置為 true 之前,工作執行緒可能會因為 _queue_.pop(task) 而進入迴圈等待的阻塞狀態(#3)。如果此時主執行緒先呼叫工作執行緒的 join() 函式(#4),將導致死鎖(deadlock)狀態。即,工作執行緒正在等待有任務入隊,而主執行緒又要等待工作執行緒的結束。為了打破迴圈等待條件,在主執行緒呼叫工作執行緒的 join() 函式之前,向佇列中放入 _workersize_ 個假任務(#5)。其目的,是確保在任務佇列上等待的所有工作執行緒退出迴圈等待(#3)。


阻塞式的執行緒安全的任務佇列 Blocking_Queue<> 如下,

template<class T>
class Blocking_Queue {

  private:
    mutex mutable _m_;            // #1
    condition_variable _cv_;
    queue<T> _q_;

  public:
    void push(T&& element) {
        lock_guard<mutex> lk(_m_);
        _q_.push(std::move(element));
        _cv_.notify_one();                // #3
    }

    void pop(T& element) {
        unique_lock<mutex> lk(_m_);
        _cv_.wait(lk, [this]{ return !_q_.empty(); });    // #2
        element = std::move(_q_.front());
        _q_.pop();
    }

    bool empty() const {
        lock_guard<mutex> lk(_m_);
        return _q_.empty();
    }

    size_t size() const {
        lock_guard<mutex> lk(_m_);
        return _q_.size();
    }

};

佇列採用了 std::mutex 和 std::condition_variable 控制工作執行緒對任務佇列的併發訪問(#1)。如果沒有可出隊的任務,當前執行緒就會在 _cv_ 上迴圈等待(#2);任務入隊後,由 _cv_ 通知在其上等待的執行緒(#3)。

邏輯

以下類圖展現了此執行緒池的程式碼主要邏輯結構。

class

[注] 圖中用構造型(stereotype)標識出工作執行緒的初始函式,並在註解中加以說明呼叫關係。

以下順序圖展現了執行緒池使用者提交任務與工作執行緒執行任務的併發過程。

sequence

驗證

驗證所使用的的測試用例及結果,與簡單的執行緒池的保持一致。

最後

完整示例請參考 [github] a_simple_thread_pool.cpp

作者參考了 C++併發程式設計實戰 / (美)威廉姆斯 (Williams, A.) 著; 周全等譯. - 北京: 人民郵電出版社, 2015.6 (2016.4重印) 一書中的部分設計思路。致 Anthony Williams、周全等譯者。

相關文章