C++ マルチスレッド学習 12 スレッド プール

はじめに
スレッドプールが必要な理由:
高い同時実行性を実行すると、スレッドの作成と破棄にコストがかかるため、事前にスレッドプールを作成し、使用するときにスレッドプールからフェッチします. 16 スレッドは共有タスクです.
list はい、タスクを追加する前に相互排除アクセスを行う必要があります

1.スレッドプールを開始します

mian() 関数で、最初にスレッド プール オブジェクトを作成します。

XThreadPool pool;

スレッドの初期化関数 init() を呼び出し、パラメーター n を init() に渡して、スレッド プールのスレッド番号メンバーに値を割り当てます。

this->thread_num_ = num;

start() を使用してスレッドを開始し、まず run でスレッド プールが初期化されているかどうかを判断し、次にスレッドが開始されているかどうかを判断します。

次に、thread_num_ 個のスレッドを作成します。各スレッドのスレッド エントリは run() です。

 auto th = make_shared<thread>(&XThreadPool::Run, this);
 threads_.push_back(th);

オブジェクトの型を指すために, 新しいスレッド オブジェクトを作成するにはエントリ関数が必要です. この関数はクラス メンバ関数なので, this ポインタも必要です. 戻り値の型はポインタ th であり, スレッド リストの threads_ が使用されます.これを管理するには:

std::vector< std::shared_ptr<std::thread> > threads_;

この時点で、スレッド プールが開始されています。

2.スレッドプールがユーザーへのサービスを開始します

main()はスレッド起動後無限ループし、ターミナルでユーザーとやり取り、
(1)Lが入力された時点でスレッドプールのtask_run_count()を呼び出し、タスク数を返す、タスク数は+1タスク実行前、および最後に 1 を引く
(2) e を入力すると、直接ブレークして無限ループを終了します
(3) v を入力すると、ビデオ トランスコーディングを開始します。まず、make_shared を使用してタスク オブジェクトを作成し、ビデオ ソースを入力し、出力サイズ、保存情報など 作成したタスクオブジェクトにこの情報を投げてから、このタスクをスレッドプールに投げ込み、スレッドプールを使ってタスクの開始時刻を判断する

//线程池线程的入口函数
void XThreadPool::Run()
{
    
    
    cout << "begin XThreadPool Run " << this_thread::get_id() << endl;
    while (!is_exit())
    {
    
    
        auto task = GetTask();
        if (!task)continue;
        ++task_run_count_;
        try
        {
    
    
            auto re = task->Run();
            //task->SetValue(re);
        }
        catch (...)
        {
    
    

        }
        --task_run_count_;
    }

    cout << "end XThreadPool Run " << this_thread::get_id() << endl;
}

スレッド エラーはこのスレッド プールの動作に影響を与えないため、例外をキャッチする必要があります。

スレッド プール内のスレッド自体にはタスクがなく、各スレッドは while() で 3 つのこと
(最初のこと) を繰り返して、GetTask() を介してスレッド プール内のタスク リスト tasks_ 内のタスクを取得します。

unique_lock<mutex> lock(mux_);
    if (tasks_.empty())
    {
    
    
        cv_.wait(lock);
    }
    if (is_exit())
        return nullptr;
    if (tasks_.empty())
        return nullptr;
    auto task = tasks_.front();
    tasks_.pop_front();
    return task;

ユーザーは大量のタスクを入力する可能性があります. マルチスレッドを使用しない場合, 一度に 1 つのタスクしか処理できません. 効率は非常に低く, それらを処理するために無限のスレッドを作成したくありません.システムがクラッシュするため、スレッド プールを使用して、一度に 16 個のタスクのみを処理できるように指定します. Tasks 、未処理のタスクはタスク キュー tasks_ にキューイングされます. タスク キューが空の場合、常に cv_ でブロックされます.wati(ロック)、ユーザーがターミナルでタスクを入力し、cv.notify を渡して次を実行するまで
(2 つ目) 取得したばかりのタスクの run() を呼び出し、ユーザーが入力した命令を例: ffmpeg
-y -i test.mp4 -s 400x300 400.mp4 >log.txt 2>&1 と入力し、system() を使用して直接システム コールを実行します。

int XVideoTask::Run()
{
    
    
    //ffmpeg -y -i test.mp4 -s 400x300 400.mp4 >log.txt 2>&1
    stringstream ss;
    ss << "ffmpeg.exe -y -i " << in_path<<" ";
    if (width > 0 && height > 0)
        ss << " -s " << width << "x" << height<<" ";
    ss << out_path;
    ss << " >" << this_thread::get_id() << ".txt 2>&1";
    return system(ss.str().c_str());
}

3番目に、スレッドプールが終了します

入力 e が終了したら、スレッド プールの stop() を呼び出します

/// 线程池退出
void XThreadPool::Stop()
{
    
    
    is_exit_ = true;
    cv_.notify_all();
    for (auto& th : threads_)
    {
    
    
        th->join();
    }
    unique_lock<mutex> lock(mux_);
    threads_.clear();
}

注:
スレッド エントリ機能と組み合わせると、次のことが理解できます。

//线程池线程的入口函数
void XThreadPool::Run()
{
    
    
    cout << "begin XThreadPool Run " << this_thread::get_id() << endl;
    while (!is_exit())
    {
    
    
        auto task = GetTask();
        if (!task)continue;
        ++task_run_count_;
        try
        {
    
    
            auto re = task->Run();
            //task->SetValue(re);
        }
        catch (...)
        {
    
    

        }
        --task_run_count_;
    }
}

01 is_exit_ = true; スレッドのrun()に無限ループを止めさせる
02 なぜ cv_.notify_all() が必要なのか;
そのような文がないと終了時にここで詰まる:
ここに画像の説明を挿入
たとえタスクがなくてもスレッドが実行されており、GetTask() が cv.wait() でスタックし、cv が彼にシグナルを送信するのを待っています. この時点で、スレッドはブロックされたままになります. 03 なぜすべてのスレッドが結合するのか
( )
スレッドが結合可能な場合、デストラクタを呼び出すとエラーが報告されます (結合可能な場合、実行が終了しても、そのスレッド リソースの解放には他のスレッドが必要です) join()) を介して完了する

四、走る

最初に 16 スレッドを開始し、次にスレッド番号
ここに画像の説明を挿入
を入力します トランスコーディング コマンドを入力し、現在実行中のタスクの数を確認し続けます。最初は 1 であり、操作が完了すると 0 と表示されます。テスト ビデオが短いため
ここに画像の説明を挿入
、針速が遅い、表示が悪い 2点中:
ここに画像の説明を挿入

5. 注意事項

(1) スレッドプールの stop() は何をしたか:
1. 最初に is_exit 変数を true に設定して、スレッドプール内のスレッドの実行が while() に入らないようにします
2. cv_.notify_all() を呼び出します; スレッド プールに入れる スレッドの get_task の cv.wait() は
スキップします 3. スレッド プール内のスレッドのリストを管理し、それらが終了するのを待ちます (実際には、スレッドが終了するのを待ちます)。これらのスレッド リソースの解放: レジスタ内の値、各スレッドに固有のスペースごとなど)

(2) 管理スレッドのリストとしてstd::vector< std::shared_ptr<std::thread> > threads_;使用する
: 1. スレッドを管理する主な目的は、終了時にこれらのスレッドを順番に結合することです. リストを一度トラバースして順番に join を呼び出すだけで済みます. 2. smart を使用
する
3. スマート ポインター リストに渡されるポインター値を作成するときのメソッド:

for (int i = 0; i < thread_num_; i++)
    {
    
    
        auto th = make_shared<thread>(&XThreadPool::Run, this);
        //shared_ptr<thread>th(new thread(&XThreadPool::Run, this));   
        threads_.push_back(th);
    }

make_shared と shared_ptr の両方を使用できますが、小さな違いがあります:
share_ptr x(new xxx()) の代わりに make_shared を使用する利点:

①パフォーマンスの向上: new で shared_ptr ポインタを構築する場合、new の処理はヒープ上でのメモリ割り当てであり、shared_ptr オブジェクトを構築する際には、ヒープ上で共有されている参照カウントを使用する必要があるため、ヒープ上でメモリを割り当てる必要があります。つまり、メモリを 2 回割り当てる必要があります。make_shared 関数を使用すると、メモリを 1 回割り当てるだけで済み、パフォーマンスが大幅に向上します。
②より安全: shared_ptr を構築する場合、(1) ヒープ メモリの新規作成、(2) メモリ空間を管理するための参照カウント領域の割り当て、この 2 つのステップの原子性は保証されませんが、最初の (1) ) ) ステップ、2 番目のステップが行われていない場合、プログラムが例外をスローすると、メモリ リークが発生するため、make_shared を使用してメモリを割り当てることをお勧めします。
③短所:make_sharedのヒープメモリの1回の割り当ては、ポインタを保持しているweak_ptrがあると参照カウントが解放されず、参照カウントと実際のオブジェクトが解放されないため、メモリ解放時にメモリの解放が遅れる可能性があります。同じヒープメモリに割り当てられているため、オブジェクトを解放することはできません.2つのメモリを別々に適用すると、解放の遅延の問題は存在しません.
まとめ:
したがって, make_shared と従来の shared_ptr 構造にはそれぞれ長所と短所があります. 通常は make_shared の方が効率的で安全なので推奨されます. メモリ解放に敏感な場合は, 通常の shared_ptr 構造を使用する必要があります.
元のリンク: https://blog.csdn.net/XiaoH0_0/article/details/101791274

4... std::vector< std::unique_ptrstd::thread > threads_; を使用することは可能ですか?
答えはイエスです。最初に uniq_ptr への直接の変更を見てください:

for (int i = 0; i < thread_num_; i++)
    {
    
    
        //auto th = new thread(&XThreadPool::Run, this);
        //auto th = make_shared<thread>(&XThreadPool::Run, this);
        //shared_ptr<thread>th(new thread(&XThreadPool::Run, this));
        unique_ptr<thread>th(new thread(&XThreadPool::Run, this));
        
        //threads_.push_back(move(th));
        threads_.push_back(th);
    }

ここに画像の説明を挿入
このエラーの理由は、unique_ptr が auto_ptr の改良版であるためです. auto_ptr がオブジェクトの所有権を譲渡すると、元の ptr のスペースにアクセスして実行時エラーが発生する可能性があります. Unique_ptr はこの欠点を改善します. コンパイル時に、オブジェクトの所有権を譲渡することは違法であると見なされます (コンパイル フェーズ エラーは潜在的なプログラム クラッシュよりも安全です)。一方、オブジェクトの譲渡は、オブジェクトを threads_ に挿入するときに呼び出され、エラーが報告されます。

unique_ptr を使用する場合は、move() を使用して右辺値に変換する必要があります (移動の利点は、プログラマーが右辺値参照を使用するコードを記述できることではなく、右辺値参照を使用して移動セマンティクスを実装できるライブラリ コードを記述できることです)。 :

for (int i = 0; i < thread_num_; i++)
    {
    
    
        //auto th = new thread(&XThreadPool::Run, this);
        //auto th = make_shared<thread>(&XThreadPool::Run, this);
        //shared_ptr<thread>th(new thread(&XThreadPool::Run, this));
        unique_ptr<thread>th(new thread(&XThreadPool::Run, this));
        
        //threads_.push_back(move(th));
        threads_.push_back(move(th));
    }

右辺値参照の役割:
+ をオーバーロードするポインター メンバー pt を持つクラス data があり、data1 の pt が指すデータを data2 の pt が指すデータに追加し、次に data2 の pt を追加できるとします。新しいオブジェクト追加されたデータを指し、2 つのデータ オブジェクトがコピー コンストラクター (create data3) のパラメーターとして一緒に追加される場合、オーバーロードされた + が最初に呼び出されてオブジェクトが取得され、次にコピー コンストラクターが呼び出されます。はメモリの無駄なので、移動セマンティクス、+ で作成されたオブジェクトの pt が指すオブジェクトを元の場所にとどめ、data3 の pt がこの pt のデータを指すようにします。物体。

移動セマンティクスを実現するには、いつコピー構築を呼び出すか、いつ呼び出さないかをコンパイラに知らせます。ここで、右辺値参照が機能します。

移動セマンティクスを使用した移動コンストラクター:
data(data&&d)
{ pt=d.pt; d.pt=nullptr; } data data3(data1+data2) が再度呼び出されると、data1+data2 は右辺値であるため、移動コンストラクターが呼び出されます。直接コピー コンストラクターの代わりに、メモリを節約します。



5. まずロックを解除してから通知する必要があるのはなぜですか
ここに画像の説明を挿入

最初に通知してからロックを解除することもできますが、相対的に言えば、消費者スレッドが最初に開始され、生産者スレッドが発行するのを待っている待機場所にすべての消費者スレッドがスタックするため、相対的に言えば時間と効率が無駄になります。通知, 特定の消費者スレッドを待つ 通知を受け取った後の次のステップは, ロックすることです. 最初に通知してからロックを解除すると, 消費者スレッドはロックを複数回取得しようとしなければならない場合があり, 一部のリソースが浪費されます.

6. get_task がキューが空かどうかを 2 回判断する必要がある理由:

unique_lock<mutex> lock(mux_);
    if (tasks_.empty())
    {
    
    
        cv_.wait(lock);
    }
    if (is_exit())
        return nullptr;
    if (tasks_.empty())
        return nullptr;
    auto task = tasks_.front();
    tasks_.pop_front();
    return task;

これは一般的なダブルチェック ロック パターンであり、シングルトン パターンのスレッド セーフな遅延パターンでも使用されます。
1つ目はパフォーマンスの問題 タスクキューが空でない場合はロックを試みる必要はない 2つ目は
相互排除アクセスの判断 タスクキューが空である2つのスレッドがここに入った場合、相互にロックする必要がある排他的アクセス拒否

おすすめ

転載: blog.csdn.net/qq_42567607/article/details/126185859