前書き:少し前に、私はメンターが欠陥検出という水平プロジェクトを行うのを手伝いました。展開するとき、推論を行うために OpenVINO を使用する予定でした。しかし、ワークピースの前後に 6 つの検出面があり、シングルスレッド推論があります。が遅すぎます。最速のワークは 9 秒以上です。次に、マルチスレッドについて考えました。マルチプロデューサー・マルチコンシューマーモデルに魔法のような変更を加えようとしていますが、変更すればするほどバグが増えます。。。それでも正直マルチスレッドをこすりました。このスレッドプールは今後もずっと使えると思って、もう少し完璧に書いてみます。さて、少し前のプロジェクトをまとめてみましょう。
スレッド プールが解決する必要がある最も重要なことは、同時読み取りと書き込みの問題であり、これはマルチスレッドでも注意を払う必要がある問題です。スレッドがタスク キューと対話するときに、読み取りと書き込みが同時に行われます。待機戦略が有効になるのはこのときです。
たとえば、タスク キューにタスクがない場合、ワーカー スレッドはタスクを取り出すまで待機する必要がありますか? タスクキューがタスクでいっぱいの場合でも、スレッドを挿入する場合は待つ必要がありますか? 待機が終了したら、1 つ以上のスレッドに通知する必要がありますか。これが待機戦略で行う必要があることです。
したがって、抽象親クラスが設計され、これらのアクションが仮想関数として記述され、サブクラスが親クラスを継承できるようになります。
class WaitStrategy {
public:
virtual void Notifyone() = 0; // 通知一个线程
virtual void BreakAllwait() = 0; // 通知全部线程
virtual bool EmptyWait() = 0; // 判断当前任务队列是否为空,如果为空则等待
virtual ~WaitStrategy(){}
};
次のステップは、どのような待機戦略を実装するかを検討することです。
一般的に使用されるのは、bolck (ブロック待機)、sleep (スレッド スリープ待機)、timeout (遅延待機) です。C++11 には、スレッド待機戦略としても使用できる yield() も用意されています。
1. ブロックはブロックされて待機中です。
class BlockWaitStrategy : public WaitStrategy {
public:
// 构造函数
BlockWaitStrategy(){}
// 只通知一个线程
void Notifyone() override{
cv_.notify_one();
}
// 通知所有线程
void BreakAllwait() override {
cv_.notify_all();
}
// 判断当前任务队列是否为空,如果为空则等待
bool EmptyWait() override{
std::unique_lock<std::mutex> lk(mutex_);
cv_.wait(lk);
return false;
}
private:
std::mutex mutex_;
std::condition_variable cv_;
};
これは実際、コード、ロック、条件変数を理解するのが非常に簡単です。唯一の複雑な点は EmptyWait() ですが、アプリケーション シナリオでは簡単に理解できます。子スレッドがタスクを取り出す必要がある場合、キューが空であることがわかり、タスクを取り出すことができないため、子スレッドはタスクを取り出すことができません。タスクがキューから取り出されるのを待ちます。待機中にタスクキューが空かどうかを判断する必要があるため、この機能を使用します。
2、スリープスリープ待機
つまり、スレッド内で sleep_for() を使用してスレッドをスリープ状態にし、スリープ時間が終了するか、現在のスレッドが条件変数によってウェイクアップされると、実行を継続します。
class SleepWaitStratrgy : public WaitStrategy
{
public:
SleepWaitStratrgy() {}
explicit SleepWaitStratrgy(std::uint64_t sleep_time_us) :sleep_time_us_(sleep_time_us) {}
// 判断当前是否为空等待
bool EmptyWait() override {
std::this_thread::sleep_for(std::chrono::microseconds(sleep_time_us_));
return true;
}
// 设置睡眠时间
void SetSleepTimeMicroSeconds(uint64_t sleep_time_us) {
sleep_time_us_ = sleep_time_us;
}
private:
std::uint64_t sleep_time_us_ = 10000;
};
ここでは、空であると判断するロジックのみを実装します。この待機戦略を使用してスレッド プール全体を実行すると、外部コードがスレッドをウェイクアップし、ここではブロック ロジックのようにウェイクアップ コードをカプセル化する必要がないからです。
3. タイムアウト遅延待機
計時にはchronoライブラリを使用し、計時時間が経過したら処理を進めます。
class TimeoutWaitStrategy : public WaitStrategy
{
public:
TimeoutWaitStrategy() {}
explicit TimeoutWaitStrategy(std::uint64_t timeout) : time_out_(std::chrono::milliseconds(timeout)) {}
// 通知一个线程
void Notifyone() override{
cv_.notify_one();
}
// 通知所有线程
void BreakAllwait() override {
cv_.notify_all();
}
// 判断队列是否为空 并进行等待操作
bool EmptyWait() override {
std::unique_lock<std::mutex> lk(mutex_);
// 等待条件变量通知,若time_out_时间内没有被通知,则返回false,队列不为空。
if (cv_.wait_for(lk, time_out_) == std::cv_status::timeout)
return false;
// 时间到了,没有被通知则返回true,队列不为空
return true;
}
// 设置阻塞时间
void SetTimeout(uint64_t timeout) {
time_out_ = std::chrono::milliseconds(timeout);
}
private:
std::mutex mutex_;
std::condition_variable cv_;
std::chrono::milliseconds time_out_;
};
4. 利回り戦略
c++11で追加されたスレッド管理手法で、現在のスレッドの実行権を放棄することで、他のスレッドに実行の機会を与え、スレッドの切り替えやスケジューリングを実現します。これは、強制的なプリエンプティブ スケジューリングではなく、スレッド認識に依存して切り替えを行う協調的なマルチタスク メカニズムです。
言い換えると、このコードが実行されると、スレッドは「一時停止」され (一時停止ではありませんが、一時停止に似ています)、現在のスレッドは独自のタイム スライスを終了し、その呼び出し優先順位をスレッド全体の最低レベルに置きます。プールにある場合は、他のスレッドを最初に実行させてから、それらのスレッドがウェイクアップされるか、独自のタイム スライスになるまで実行します。
class YieldWaitStrategy : public WaitStrategy
{
public:
YieldWaitStrategy(){}
bool EmptyWait() override {
std::this_thread::yield();
return true;
}
};
ここでは空判定のロジックを実装しただけですが、理由は睡眠のロジックと同じです。すべては、自然なウェイクアップまたは外部のウェイクアップを待っている「一時停止」されたスレッドです。