web-dev-qa-db-ja.com

ループC ++でのスレッドの再利用

C++プログラムでいくつかのタスクを並列化する必要があり、並列プログラミングはまったく新しいです。私はこれまでインターネット検索を介していくつかの進歩を遂げましたが、今は少し行き詰まっています。いくつかのスレッドをループで再利用したいのですが、自分が何をしようとしているのか、はっきりとわかりません。

コンピューター上の2つのADCカードからデータを取得し(並列で取得)、次のバッチのデータを収集しながら、収集されたデータ(並列処理)に対していくつかの操作を実行する必要があります。これは、説明するためのいくつかの疑似コードです

//Acquire some data, wait for all the data to be acquired before proceeding
std::thread acq1(AcquireData, boardHandle1, memoryAddress1a);
std::thread acq2(AcquireData, boardHandle2, memoryAddress2a);
acq1.join();
acq2.join();

while(user doesn't interrupt)
{

//Process first batch of data while acquiring new data
std::thread proc1(ProcessData,memoryAddress1a);
std::thread proc2(ProcessData,memoryAddress2a);
acq1(AcquireData, boardHandle1, memoryAddress1b);
acq2(AcquireData, boardHandle2, memoryAddress2b);
acq1.join();
acq2.join();
proc1.join();
proc2.join();
/*Proceed in this manner, alternating which memory address 
is written to and being processed until the user interrupts the program.*/
}

それが主な要点です。ループの次の実行は、「b」データの処理中に「a」のメモリアドレスに書き込み、代替を続行します(コードを実行して、問題を混乱させないように取り出しました)。

とにかく、(一部の人はすでに知っていると思いますが)2回目にacq1とacq2を使用しようとすると、コンパイラ(VS2012)は「IntelliSense:適切な演算子なしのクラス型のオブジェクトの呼び出し( )または関数へのポインターへの変換関数」。同様に、再びstd :: threadをacq1とacq2の前に置くと、「エラーC2374: 'acq1':再定義;複数の初期化」と表示されます。

つまり、問題は、前のタスクが完了したときにスレッドを新しいタスクに再割り当てできるかどうかです。スレッドを再度呼び出す前に、スレッドの以前の使用が終了するのを常に待ちますが、スレッドを再割り当てする方法がわかりません。また、ループ内にあるため、毎回新しいスレッドを作成できません(またはそれは無駄で不必要に思えますが、私は間違っている可能性があります)。

前もって感謝します

19
notaCSmajor

最も簡単な方法は、std::functionオブジェクトの待機可能なキューを使用することです。このような:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <chrono>


class ThreadPool
{
    public:

    ThreadPool (int threads) : shutdown_ (false)
    {
        // Create the specified number of threads
        threads_.reserve (threads);
        for (int i = 0; i < threads; ++i)
            threads_.emplace_back (std::bind (&ThreadPool::threadEntry, this, i));
    }

    ~ThreadPool ()
    {
        {
            // Unblock any threads and tell them to stop
            std::unique_lock <std::mutex> l (lock_);

            shutdown_ = true;
            condVar_.notify_all();
        }

        // Wait for all threads to stop
        std::cerr << "Joining threads" << std::endl;
        for (auto& thread : threads_)
            thread.join();
    }

    void doJob (std::function <void (void)> func)
    {
        // Place a job on the queu and unblock a thread
        std::unique_lock <std::mutex> l (lock_);

        jobs_.emplace (std::move (func));
        condVar_.notify_one();
    }

    protected:

    void threadEntry (int i)
    {
        std::function <void (void)> job;

        while (1)
        {
            {
                std::unique_lock <std::mutex> l (lock_);

                while (! shutdown_ && jobs_.empty())
                    condVar_.wait (l);

                if (jobs_.empty ())
                {
                    // No jobs to do and we are shutting down
                    std::cerr << "Thread " << i << " terminates" << std::endl;
                    return;
                 }

                std::cerr << "Thread " << i << " does a job" << std::endl;
                job = std::move (jobs_.front ());
                jobs_.pop();
            }

            // Do the job without holding any locks
            job ();
        }

    }

    std::mutex lock_;
    std::condition_variable condVar_;
    bool shutdown_;
    std::queue <std::function <void (void)>> jobs_;
    std::vector <std::thread> threads_;
};

void silly (int n)
{
    // A silly job for demonstration purposes
    std::cerr << "Sleeping for " << n << " seconds" << std::endl;
    std::this_thread::sleep_for (std::chrono::seconds (n));
}

int main()
{
    // Create two threads
    ThreadPool p (2);

    // Assign them 4 jobs
    p.doJob (std::bind (silly, 1));
    p.doJob (std::bind (silly, 2));
    p.doJob (std::bind (silly, 3));
    p.doJob (std::bind (silly, 4));
}
28
David Schwartz

_std::thread_クラスは、1つのタスク(コンストラクターで指定したタスク)を実行して終了するように設計されています。さらに作業をしたい場合は、新しいスレッドが必要になります。 C++ 11の時点では、これですべてです。スレッドプールは標準になりませんでした。 (C++ 14がそれらについて何を言わなければならないかは不明です。)

幸い、必要なロジックを自分で簡単に実装できます。大規模な画像は次のとおりです。

  • 開始nすべてが以下を実行するワーカースレッド:
    • やるべきことがまだある間に繰り返します:
      • 次のタスクをつかみますt(おそらく準備ができるまで待機しています)。
      • プロセスt
  • 新しいタスクを処理キューに挿入し続けます。
  • ワーカースレッドに、これ以上行うことはないことを伝えます。
  • ワーカースレッドが完了するまで待ちます。

ここで最も難しい部分(それでもかなり簡単です)は、ワークキューを適切に設計することです。通常、同期されたリンクリスト(STLから)がこれを行います。同期とは、キューを操作するスレッドは、_std::mutex_を取得してから競合状態を回避するようにしてください。ワーカースレッドがリストが空であると判断した場合は、再び作業が行われるまで待機する必要があります。これには_std::condition_variable_を使用できます。新しいタスクがキューに挿入されるたびに、挿入スレッドnotifyは、条件変数で待機するため、ブロッキングを停止し、最終的に新しいタスクの処理を開始します。

2番目のそれほど重要な部分は、実行する必要のある作業がなくなったことをワーカースレッドに通知する方法です。明らかに、いくつかのグローバルフラグを設定できますが、ワーカーがキューでの待機をブロックされている場合、すぐには認識されません。 1つの解決策は、notify_all()スレッドを使用して、通知されるたびにフラグをチェックすることです。別のオプションは、いくつかの明確な「有毒」アイテムをキューに挿入することです。ワーカーがこのアイテムに遭遇すると、それ自体が終了します。

タスクのキューを表すことは、自分で定義したtaskオブジェクトまたは単にラムダを使用することで簡単です。

上記はすべてC++ 11の機能です。以前のバージョンに行き詰まっている場合は、特定のプラットフォームにマルチスレッドを提供するサードパーティのライブラリに頼る必要があります。

これはどれもロケット科学ではありませんが、それでも初めての間違いは簡単です。残念ながら、同時実行性に関連するバグはデバッグが最も困難なものの1つです。良い本の関連セクションを読んだり、チュートリアルを読んだりして数時間を費やすことから始めれば、すぐに報われるでしょう。

9
5gon12eder

まあ、それはあなたが再割り当てを移動することを検討するかどうかに依存します。スレッドは移動できますが、コピーは作成できません。

以下のコードは、反復ごとに新しいスレッドのペアを作成し、古いスレッドの代わりにそれらを移動します。新しいthreadオブジェクトは一時的なものになるので、これでうまくいくと思います。

while(user doesn't interrupt)
{
//Process first batch of data while acquiring new data
std::thread proc1(ProcessData,memoryAddress1a);
std::thread proc2(ProcessData,memoryAddress2a);
acq1 = std::thread(AcquireData, boardHandle1, memoryAddress1b);
acq2 = std::thread(AcquireData, boardHandle2, memoryAddress2b);
acq1.join();
acq2.join();
proc1.join();
proc2.join();
/*Proceed in this manner, alternating which memory address 
is written to and being processed until the user interrupts the program.*/
}

何が起こっているのかというと、ループに関して外側のスコープで宣言されているため、オブジェクトは実際には反復の最後でその寿命を終了しません。しかし、毎回新しいオブジェクトが作成され、moveが発生します。何が節約できるかわからないので(私は愚かかもしれません)、これはループ内でacqsを宣言して単純にシンボルを再利用するのとまったく同じだと思います。結局のところ、そうですね、それは、一時的な作成とmoveをどのように分類するかについてです。

また、これはループごとに新しいスレッドを明確に開始し(もちろん、以前に割り当てられたスレッドを終了します)、スレッドが新しいデータを待機するのではなく、魔法のように処理パイプにフィードされます。別の方法で実装する必要があります。例:ワーカースレッドプールとキューを介した通信。

参照: operator=(ctor)

あなたが得るエラーは自明であると思うので、それらの説明はスキップします。

0
luk32

この

 std::thread acq1(...)

コンストラクタの呼び出しです。 acq1という新しいオブジェクトを作成する

この

  acq1(...)

()演算子を既存のオブジェクトaqc1に適用したものです。 std :: threadにそのような演算子が定義されていない場合、コンパイラーは不平を言います。

私の知る限り、std :: threadsを再利用しないでください。あなたはそれらを構築して起動します。彼らと一緒になって捨てて

0
Oncaphillis