最初に少しcontext:私はC++ 11のスレッド化について学習中です。この目的のために、基本的に小さなactor
クラスを構築しようとしています(私は例外処理と伝播のものを省きました)のように:
_class actor {
private: std::atomic<bool> stop;
private: std::condition_variable interrupt;
private: std::thread actor_thread;
private: message_queue incoming_msgs;
public: actor()
: stop(false),
actor_thread([&]{ run_actor(); })
{}
public: virtual ~actor() {
// if the actor is destroyed, we must ensure the thread dies too
stop = true;
// to this end, we have to interrupt the actor thread which is most probably
// waiting on the incoming_msgs queue:
interrupt.notify_all();
actor_thread.join();
}
private: virtual void run_actor() {
try {
while(!stop)
// wait for new message and process it
// but interrupt the waiting process if interrupt is signaled:
process(incoming_msgs.wait_and_pop(interrupt));
}
catch(interrupted_exception) {
// ...
}
};
private: virtual void process(const message&) = 0;
// ...
};
_
すべてのアクターは、独自の_actor_thread
_で実行され、_incoming_msgs
_の新しい着信メッセージを待機し、メッセージが到着すると処理します。
_actor_thread
_はactor
と一緒に作成され、一緒に死ななければなりません。そのため、message_queue::wait_and_pop(std::condition_variable interrupt)
に何らかの割り込みメカニズムが必要です。
基本的に、a)新しいmessage
が到着するか、b)interrupt
が発火するまで_wait_and_pop
_ブロックする必要があります。その場合、理想的には_interrupted_exception
_がスローされます。
_message_queue
_への新しいメッセージの到着は、現在_std::condition_variable new_msg_notification
_によってもモデル化されています:
_// ...
// in class message_queue:
message wait_and_pop(std::condition_variable& interrupt) {
std::unique_lock<std::mutex> lock(mutex);
// How to interrupt the following, when interrupt fires??
new_msg_notification.wait(lock,[&]{
return !queue.empty();
});
auto msg(std::move(queue.front()));
queue.pop();
return msg;
}
_
簡単に言えば、questionはこれです:interrupt
がトリガーされたときに、new_msg_notification.wait(...)
の新しいメッセージの待機を中断するにはどうすればよいですかタイムアウト)?
あるいは、質問は次のように読むこともできます。2つの_std::condition_variable
_ sのいずれかが通知されるまでどのように待つのですか?
単純なアプローチの1つは、割り込みに_std::condition_variable
_をまったく使用せず、代わりにアトミックフラグ_std::atomic<bool> interrupted
_を使用し、次のいずれかになるまで非常に短いタイムアウトで_new_msg_notification
_を待機することです新しいメッセージが到着したか、_true==interrupted
_までです。しかし、私は忙しい待機を避けたいです。
Pilcrowのコメントと回答から、基本的に2つのアプローチが考えられます。
興味のある方のために、私の実装は次のようになります。私の場合の条件変数は、実際にはsemaphore
です(私はそれらがより好きで、そうすることの練習が好きだったので)。このセマフォには、semaphore::get_interrupt()
を介してセマフォから取得できるinterrupt
が関連付けられています。あるスレッドがsemaphore::wait()
でブロックすると、別のスレッドがセマフォの割り込みでsemaphore::interrupt::trigger()
を呼び出す可能性があり、最初のスレッドが_interrupt_exception
_のブロックを解除して伝播します。
_struct
interrupt_exception {};
class
semaphore {
public: class interrupt;
private: mutable std::mutex mutex;
// must be declared after our mutex due to construction order!
private: interrupt* informed_by;
private: std::atomic<long> counter;
private: std::condition_variable cond;
public:
semaphore();
public:
~semaphore() throw();
public: void
wait();
public: interrupt&
get_interrupt() const { return *informed_by; }
public: void
post() {
std::lock_guard<std::mutex> lock(mutex);
counter++;
cond.notify_one(); // never throws
}
public: unsigned long
load () const {
return counter.load();
}
};
class
semaphore::interrupt {
private: semaphore *forward_posts_to;
private: std::atomic<bool> triggered;
public:
interrupt(semaphore *forward_posts_to) : triggered(false), forward_posts_to(forward_posts_to) {
assert(forward_posts_to);
std::lock_guard<std::mutex> lock(forward_posts_to->mutex);
forward_posts_to->informed_by = this;
}
public: void
trigger() {
assert(forward_posts_to);
std::lock_guard<std::mutex>(forward_posts_to->mutex);
triggered = true;
forward_posts_to->cond.notify_one(); // never throws
}
public: bool
is_triggered () const throw() {
return triggered.load();
}
public: void
reset () throw() {
return triggered.store(false);
}
};
semaphore::semaphore() : counter(0L), informed_by(new interrupt(this)) {}
// must be declared here because otherwise semaphore::interrupt is an incomplete type
semaphore::~semaphore() throw() {
delete informed_by;
}
void
semaphore::wait() {
std::unique_lock<std::mutex> lock(mutex);
if(0L==counter) {
cond.wait(lock,[&]{
if(informed_by->is_triggered())
throw interrupt_exception();
return counter>0;
});
}
counter--;
}
_
このsemaphore
を使用すると、メッセージキューの実装は次のようになります(_std::condition_variable
_の代わりにセマフォを使用すると、_std::mutex
_を削除できます。
_class
message_queue {
private: std::queue<message> queue;
private: semaphore new_msg_notification;
public: void
Push(message&& msg) {
queue.Push(std::move(msg));
new_msg_notification.post();
}
public: const message
wait_and_pop() {
new_msg_notification.wait();
auto msg(std::move(queue.front()));
queue.pop();
return msg;
}
public: semaphore::interrupt&
get_interrupt() const { return new_msg_notification.get_interrupt(); }
};
_
私のactor
は、非常に低いレイテンシでスレッドを中断できるようになりました。現在、この実装は次のようになっています。
_class
actor {
private: message_queue
incoming_msgs;
/// must be declared after incoming_msgs due to construction order!
private: semaphore::interrupt&
interrupt;
private: std::thread
my_thread;
private: std::exception_ptr
exception;
public:
actor()
: interrupt(incoming_msgs.get_interrupt()), my_thread(
[&]{
try {
run_actor();
}
catch(...) {
exception = std::current_exception();
}
})
{}
private: virtual void
run_actor() {
while(!interrupt.is_triggered())
process(incoming_msgs.wait_and_pop());
};
private: virtual void
process(const message&) = 0;
public: void
notify(message&& msg_in) {
incoming_msgs.Push(std::forward<message>(msg_in));
}
public: virtual
~actor() throw (interrupt_exception) {
interrupt.trigger();
my_thread.join();
if(exception)
std::rethrow_exception(exception);
}
};
_
あなたが尋ねる、
C++ 11で複数の条件変数を待機する最良の方法は何ですか?
できませんし、再設計する必要があります。 1つのスレッドは、一度に1つの条件変数(および関連するミューテックス)のみを待機できます。これに関して、同期のためのWindows機能は、同期プリミティブの「POSIXスタイル」ファミリーの機能よりもかなり豊富です。
スレッドセーフキューの典型的なアプローチは、特別な「すべて完了」をキューに入れることです。メッセージ、または「ブレイカブル」(または「シャットダウン可能」)キューを設計します。後者の場合、キューの内部条件変数は複雑な述語を保護します:アイテムが利用可能かまたはキューが壊れています。
コメントであなたはそれを観察する
待機している人がいない場合、notify_all()は効果がありません
それは本当ですが、おそらく関係ありません。条件変数のwait()
ingは、述語をチェックし、それをチェックすることも意味しますbefore実際に通知をブロックします。そのため、notify_all()
を「欠落」するキューアイテムの処理でビジー状態のワーカースレッドは、次にキューの状態を検査するときに、述語(新しいアイテムが利用可能、またはキューがすべて完了)が変更されました。
最近、プロデューサー/ワーカーごとに単一の条件変数と個別のブール変数を使用してこの問題を解決しました。コンシューマスレッドの待機関数内の述語は、これらのフラグを確認し、どのプロデューサー/ワーカーが条件を満たしたかを判断できます。
たぶんこれはうまくいくかもしれません:
割り込みを取り除きます。
_ message wait_and_pop(std::condition_variable& interrupt) {
std::unique_lock<std::mutex> lock(mutex);
{
new_msg_notification.wait(lock,[&]{
return !queue.empty() || stop;
});
if( !stop )
{
auto msg(std::move(queue.front()));
queue.pop();
return msg;
}
else
{
return NULL; //or some 'terminate' message
}
}
_
デストラクタで、interrupt.notify_all()
をnew_msg_notification.notify_all()
に置き換えます