C++11封裝線程池(2)

先上代碼,代碼來自GitHub。這段代碼用了大量C++ 11新特性,並且非常晦澀難懂,接下來會對每個細節逐個解釋。

ThreadPool.h

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

class ThreadPool {
public:
    ThreadPool(size_t);
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)
        ->std::future<typename std::result_of<F(Args...)>::type>;
    ~ThreadPool();
private:
    // need to keep track of threads so we can join them
    std::vector< std::thread > workers;
    // the task queue
    std::queue< std::function<void()> > tasks;

    // synchronization
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
    : stop(false)
{
    for (size_t i = 0; i<threads; ++i)
        workers.emplace_back(
        [this]
    {
        for (;;)
        {
            std::function<void()> task;

            {
                std::unique_lock<std::mutex> lock(this->queue_mutex);
                this->condition.wait(lock,
                    [this]{ return this->stop || !this->tasks.empty(); });
                if (this->stop && this->tasks.empty())
                    return;
                task = std::move(this->tasks.front());
                this->tasks.pop();
            }

            task();
        }
    }
    );
}

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;

    auto task = std::make_shared< std::packaged_task<return_type()> >(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        // don't allow enqueueing after stopping the pool
        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        tasks.emplace([task](){ (*task)(); });
    }
    condition.notify_one();
    return res;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread &worker : workers)
        worker.join();
}

#endif



main.c

#include <iostream>
#include <vector>
#include <chrono>

#include "ThreadPool.h"

int main()
{

    ThreadPool pool(4);
    std::vector< std::future<int> > results;

    for (int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
            std::cout << "hello " << i << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "world " << i << std::endl;
            return i*i;
        })
            );
    }

    for (auto && result : results)
        std::cout << result.get() << ' ';
    std::cout << std::endl;

    getchar();
    return 0;
}



詳細分析
從 ThreadPool.h 開始逐段分析。

#include <vector>               // 數組
#include <queue>                // 隊列
#include <memory>               // 內存操作
#include <thread>               // 線程相關
#include <mutex>                // 互斥量
#include <condition_variable>   // 條件變量
#include <future>               // 從異步獲取結果
#include <functional>           // 包裝函數爲對象
#include <stdexcept>            // 異常相關

>詳解<

1 < thread>: 是 C++ 11的新特性,主要包含了線程對象std::thread的構造。 
2 < mutex>: C++ 11新特性,主要包含各種Mutex的類的構造,主要是std::mutex。 
3 < condition_variable>: C++ 11新特性, 包含多線程中常用的條件變量的聲明,例如notify_one、wait、wait_for等等。 
4 < future>: C++ 11新特性,可以獲取異步任務的結果,可用來實現同步。包括std::sync和std::future。 
5 < functional>: C++ 11增加了一些新特性,簡單來說可以實現函數到對象的綁定,如bind()函數。

然後是對線程池對象ThreadPool的聲明:

class ThreadPool {
public:
    ThreadPool(size_t);    // 構造函數
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args)
        ->std::future<typename std::result_of<F(Args...)>::type>; // 模板,下面會有詳解
    ~ThreadPool(); // 析構
private:
    // need to keep track of threads so we can join them
    std::vector< std::thread > workers;    // 線程數組
    // the task queue
    std::queue< std::function<void()> > tasks; // 任務隊列,幾乎所有的線程池都會有任務隊列

    // synchronization
    std::mutex queue_mutex;    // 互斥量
    std::condition_variable condition; // 條件變量
    bool stop; // 停止或開始任務
};

>詳解<

1 template < class F, class… Args> auto enqueue(F&& f, Args&&… args) -> std::future< typename std::result_of< F(Args…)>::type>; 
理解了這一句,這個程序就差不多弄懂了。 
首先,這是一個函數模板,而不是類模板。 
template<> 部分: template < class F, class… Args>。class… Args代表接受多個參數。 
返回類型: auto 
函數名: enqueue 
形參表: (F&& f, Args&&… args)。&&是C++ 11新特性,代表右值引用。 
不明覺厲: -> std::future< typename std::result_of< F(Args…)>::type>。這個->符號其實用到了C++ 11中的lamda表達式,後面的內容代表函數的返回類型。 
總的來說就是,這句話聲明瞭一個名爲enqueue()的函數模板,它的模板類型爲class F以及多個其他類型Args,它的形參是一個F&&類型的f以及多個Args&&類型的args,最後這個函數返回類型是std::future< typename std::result_of < F(Args…)>::type >。有點非人類。 
對於這個冗長的返回類型,又可以繼續分析: 
std::future在前面提到過了,它本身是一個模板,包含在 < future>中。通過std::future可以返回這個A類型的異步任務的結果。 
std::result_of\::type就是這段代碼中的A類型。result_of獲取了someTask的執行結果的類型。 
F(Args…)_就是這段代碼的someTask,即函數F(Args…)。 
所以最後這個模板函數enqueue()的返回值類型就是F(Args…)的異步執行結果類型。 
2 std::vector < std::thread> workers: 像註釋說的那樣,用來保存線程對象 
3 std::queue < std::function\void()>> tasks: 任務隊列 
4 queue_mutex和condition: 線程同步需要的變量

接下來是構造函數ThreadPool(size_t threads)

// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
    : stop(false)
{
    for (size_t i = 0; i<threads; ++i)
        workers.emplace_back(
        [this]
    {
        for (;;)
        {
            std::function<void()> task;

            {
                std::unique_lock<std::mutex> lock(this->queue_mutex);
                this->condition.wait(lock,
                    [this]{ return this->stop || !this->tasks.empty(); });
                if (this->stop && this->tasks.empty())
                    return;
                task = std::move(this->tasks.front());
                this->tasks.pop();
            }

            task();
        }
    }
    );
}

構造函數聲明爲inline, 函數體內存在lambda表達式。 
>詳解<

1 inline: 類似宏定義,會建議編譯器把函數以直接展開的形式放入目標代碼而不是以入棧調用的形式。通常函數體內代碼比較長或者體內出現循環時不宜使用內聯,這樣會造成代碼膨脹。具體參考《Effective C++》: 第30條 。 
2 workers.emplace_back([this]{…}); 
emplace_back()與push_back()類似,但是前者更適合用來傳遞對象,因爲它可以避免對象作爲參數被傳遞時在拷貝成員上的開銷。 
這裏emplace_back()了一個lambda表達式[this]{…}。lambda表達式本身代表一個匿名函數(即沒有函數名的函數),通常格式爲[捕獲列表](參數列表)->return 返回類型{函數體}。而在本代碼中的lambda表達式是作爲一個線程放入workers[]中。 
這個線程是個for(;;)循環。 
3 for(;;)裏面: 每次循環首先聲明一個std::function< void()> task,task是一個可以被封裝成對象的函數,在此作爲最小任務單位。然後用{}添加了一個作用域。 
4 作用域裏面: 在這個作用域中進行了一些線程上鎖和線程狀態的判斷。 
5 lock(this->queue_mutex): 聲明上鎖原語 
6 condition.wait(lock, [this]{…}): 使當前線程進入阻塞狀態: 當第二個參數爲false時,wait()會阻塞當前線程,爲true時解除阻塞;在本例中的條件就是,當線程池運行或者任務列表爲空時,線程進入阻塞態。 
然後判斷,如果線程池運行或者任務列表爲空則繼續後續操作,否則退出這個[this]{…}線程函數。 
std::move()是移動構造函數,相當於效率更高的拷貝構造函數。最後將tasks[]任務隊列的第一個任務出棧。 
7 離開作用域: 然後執行task(),當前一輪循環結束。

添加新任務到線程池中的模板函數enqueue()的實現:

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;

    auto task = std::make_shared< std::packaged_task<return_type()> >(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

    std::future<return_type> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        // don't allow enqueueing after stopping the pool
        if (stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        tasks.emplace([task](){ (*task)(); });
    }
    condition.notify_one();
    return res;
}

模板函數體外的部分已經分析過了,來看看函數體內做了什麼。 
>詳解<

1 using … = typename …; 功能類似typedef。將return_type聲明爲一個result_of< F(Args…)>::type類型,即函數F(Args…)的返回值類型。 
2 make_shared < packaged_task < >>(bind()): 又是複雜的嵌套。 
make_shared : 開闢()個類型爲<>的內存 
packaged_task : 把任務打包,這裏打包的是return_type 
bind : 綁定函數f, 參數爲args… 
forward : 使()轉化爲<>相同類型的左值或右值引用 
簡單來說,這句話相當於把函數f和它的參數args…打包爲一個模板內定義的task,便於後續操作。 
3 res = task->get_future(): 與模板函數的返回類型一致,是函數異步的執行結果。 
4 新作用域: 先是一個加鎖原語lock()。 
然後是個異常處理,如果停止的話拋出一個運行時異常。 
最後,向任務列表插入這個任務[task](){(*task)();}。 
5 condition.notify_one(): 解除一個正在等待喚醒的線程的阻塞態。 
6 返回異步結果res

析構函數:

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread &worker : workers)
        worker.join();
}

>詳解<

加鎖原語lock(queue_mutex) 
解除所有線程的阻塞態notify_all() 
當所有線程執行完畢時返回主線程worker.join()

再看看main.c:

    ThreadPool pool(4);
    std::vector< std::future<int> > results;

聲明一個有4個線程的線程池。 
results[]用來保存異步調用的結果。

調用線程池

for (int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
            std::cout << "hello " << i << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "world " << i << std::endl;
            return i*i;
        })
      );
}

這個例子裏添加到線程池中的任務函數返回類型是int, 函數體是打印hello world和i以及暫停一秒。

for (auto && result : results)
        std::cout << result.get() << ' ';
    std::cout << std::endl;

將所有的線程返回結果打印出來,打印結果也是異步執行的。

原文鏈接:https://blog.csdn.net/GavinGreenson/article/details/72770818

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