web-dev-qa-db-ja.com

C ++ 11で複数の条件変数を待つ最良の方法は何ですか?

最初に少し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つのアプローチが考えられます。

  1. Alan、mukunda、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);
    }
};
_
27
Julian

あなたが尋ねる、

C++ 11で複数の条件変数を待機する最良の方法は何ですか?

できませんし、再設計する必要があります。 1つのスレッドは、一度に1つの条件変数(および関連するミューテックス)のみを待機できます。これに関して、同期のためのWindows機能は、同期プリミティブの「POSIXスタイル」ファミリーの機能よりもかなり豊富です。

スレッドセーフキューの典型的なアプローチは、特別な「すべて完了」をキューに入れることです。メッセージ、または「ブレイカブル」(または「シャットダウン可能」)キューを設計します。後者の場合、キューの内部条件変数は複雑な述語を保護します:アイテムが利用可能かまたはキューが壊れています。

コメントであなたはそれを観察する

待機している人がいない場合、notify_all()は効果がありません

それは本当ですが、おそらく関係ありません。条件変数のwait() ingは、述語をチェックし、それをチェックすることも意味しますbefore実際に通知をブロックします。そのため、notify_all()を「欠落」するキューアイテムの処理でビジー状態のワーカースレッドは、次にキューの状態を検査するときに、述語(新しいアイテムが利用可能、またはキューがすべて完了)が変更されました。

15
pilcrow

最近、プロデューサー/ワーカーごとに単一の条件変数と個別のブール変数を使用してこの問題を解決しました。コンシューマスレッドの待機関数内の述語は、これらのフラグを確認し、どのプロデューサー/ワーカーが条件を満たしたかを判断できます。

3
Aditya Kumar

たぶんこれはうまくいくかもしれません:

割り込みを取り除きます。

_ 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()に置き換えます

0
Davidb