C++11実装の100ラインスレッドプール解析


C++はスレッドの操作を持っていて、非同期の操作、スレッドのプールがなくて、スレッドのプールの概念について、私はまず他の人の解釈を探します:一般的に言えば、スレッドのプールは以下のいくつかの部分があります:1.プライマリ・タスクの1つ以上のスレッドを完了します.2.スケジューリング管理用の管理スレッド.3.実行を要求するタスクキュー私は人の話をします:あなたの関数はマルチスレッドの中で実行する必要がありますが、あなたはまた1つの関数ごとに1つのスレッドを開くことができません.だから、固定したN個のスレッドを実行する必要があります.しかし、一部のスレッドはまだ実行していません.あるスレッドはまだ実行していません.あるスレッドは暇です.どのようにタスクを割り当てますか.スレッドプールをカプセル化してこれらの操作を完了する必要があります.スレッドプールというレイヤがカプセル化されている場合は、いくつかのスレッドを開いて、直接タスクを塞いで、一定のメカニズムで実行結果を取得するように伝えるだけです.スレッドプールを実装する100行のアクションがあります.
https://github.com/progschj/ThreadPool/blob/master/ThreadPool.h
ソースヘッダファイルの解析
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

vector,queue,momoryは何も言っていませんthreadスレッド関連、mutex反発量、リソース占有問題の解決、condition_variable条件量は、スレッドを起動したりブロックしたりするために使用され、futureは使用の観点からスレッドデータを取得する関数です.functional関数子は,正規化された関数ポインタとして理解できる.stdexceptはその名前と同じで、標準異常です.
class ThreadPool {
public:
    ThreadPool(size_t);
    template
    auto enqueue(F&& f, Args&&... args) 
        -> std::future::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 > tasks;
    
    // synchronization
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

スレッドプールの宣言、コンストラクション関数、enqueueテンプレート関数はstd::futureを返し、このtypeは実行時検出(コンパイル時検出)を利用します.推測したのは、とてもamazingですね.1行のコードを繰り返し使用することに成功しました.この高次の使い方は大物のレベルですか.i了iです.workersはvector<:thread>通称ワークスレッドです.std::queue<:function>>tasks通称タスクキュー.では問題ですが、このタスクキューのタスクはvoid()タイプしかありませんか?そんなに簡単ではないような気がしますが、続けて見なければなりませんね.mutex,condition_variableは何も言わないで、stop制御スレッドプールが停止しました.
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads)
    :   stop(false)
{
    for(size_t i = 0;i task;

                    {
                        std::unique_lock<: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();
                }
            }
        );
}

このコンストラクション関数は一定数のスレッドを詰め込むだけだと言っていますが、私は見てから気づいたのですが、これはどういう意味なのか......本質的にはスレッドを詰め込むだけですが、このスレッドはあまりにも迂回しています.workers.emplace_backパラメータはlambda式であり、ブロックされません.つまり、最外層は非同期関数であり、各スレッドの中のことがポイントです.labmda式の最外層はデッドサイクルであり,なぜfor(;;)ホワイトではありません(1)これはポイントではありませんが、大物の使い方は推測に値します.効率がもっと高いと思います.taskが明らかにした後、大きな括弧に続いて、この{}の中の部分は、同期操作であり、this->lockを直接使用するのではなく[&]でパラメータをキャプチャする理由についても、メモリの考慮にあると思います.細かく計算するスタイルはけちけちした地主に似ていて、i了i了.wait(lock,condtion)の操作に続いて、千層餅のセットのようです.第1階:このTMは自分をロックするのではないでしょうか.これは構造が詰まっているのではないでしょうか.階層2:emplace_backのスレッドは、ブロックされませんが、ロックを開くのを待って、ロックは自分のスレッドの中にあるのではないでしょうか.じゃあ鍵をかけないといけないの?第3層:私たちはこのlockが実はただの包装であることを見て、本当のロックは外層のmutexなので、ここからデッドロックは存在しません.しかし、あなたのwaitのconditionが分からないわけがないでしょう.stopか!emptyはwaitですか?第四層:私たちは資料を調べて、後ろのconditionがfalseに戻ってからwaitすることを発見しました.つまり!stop&&emptyはwaitします.つまり、このスレッドプールは実行状態であり、待機操作を実行するタスクはありません.さもないと待たないで、直接突き進みます!第五層:stopと非空を判断した以上、なぜstopと空を判断して脱退するのか.冗長に見えない?階層6:確かにstopに設定されており、キューが空になっていることを確認してこそ、退職を光栄に思うことができます.問題はありませんか.はい、最後にすべてのスレッドがブロックされています.stopをtrueにしてもわかりませんね.stopはすべてのスレッドを呼び覚ます操作があると思いますが、実行しているものもあれば、待っているものもあれば、通知できないはずですが、次の判断で正常に終了することができます.疑惑があったのでstop関連の操作を見てみたかったのですが、析構ハ数の中に入っていることに気づきました・・・
// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<:mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers)
        worker.join();
}

{}中に鍵がかかってstopがtrueの操作が行われていますが、なぜ原子で操作しないのかは分かりませんが、よく考えてみるともともと鍵がかかっているので、原子では内臭ではありません.そしてそれはやはりすべてを通知し、作業スレッドjoinを通知しました.つまり、それらが終わるのを待っています.千層餅の解析を終えた後、最も重要な入隊操作を見てみましょう.
// add new work item to the pool
template
auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future::type>
{
    using return_type = typename std::result_of::type;

    auto task = std::make_shared< std::packaged_task >(
            std::bind(std::forward(f), std::forward(args)...)
        );
        
    std::future res = task->get_future();
    {
        std::unique_lock<: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;
}
typename std::result_of::typeのtypenameは曖昧さを解消するためか、名前に依存する関係をネストしているため、テンプレートを断固として書かない普通のプログラマーとして、このコードは難しすぎます......-> type私は何が起こっているのか知っています.それは、その戻りタイプを示す方法resultです.ofはFがArgsとして署名された関数であることを示すはずです...このパラメータは、Argsが何であるかは関係なく、戻り値のパラメータタイプに関心を持っているのでtypeがあります.なぜ関数エントリが右値参照なのかについては、私の理解の範囲を超えています.まさかfunctionalは右の値で引用しなければなりませんか?では、その廃棄は誰が管理しますか?このスレッドは管理しますか?これらの穴は後でゆっくり埋めます.前にtasksはvoid()の関数タイプしか受信できないと述べましたが、ここではstd::packaged_taskを使用して関数タイプの導出を完了します.なぜfunctionを使用しないのかというと、これは最終的にtasksのオブジェクトに入るのではなく、futureに戻る作業を引き継ぐためです.package_taskはfutureに戻ります・・・そして + + futureの操作です.もともとスレッドプールの最も理解しにくい部分で、かえって平凡に見えます.前の派手な操作が私たちの理解能力をよく通じているからです.この操作にはもともと少し概念があり、「これだけ?」今日も出勤して魚を触る日です.