在C ++ 11中等待多个条件变量的最佳方法是什么?

时间:2014-12-07 08:40:28

标签: c++ multithreading c++11 synchronization condition-variable

首先是一个上下文:我正在学习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都在自己的actor_thread中运行,等待incoming_msgs上的新传入消息,并且 - 当消息到达时 - 处理它。

actor_threadactor一起创建,并且必须与它一起死亡,这就是我需要message_queue::wait_and_pop(std::condition_variable interrupt)中某种中断机制的原因。

基本上,我需要wait_and_pop阻止,直到任何一个 a)新的message到达或 b)直到interrupt被解雇,在这种情况下 - 理想情况下 - 要抛出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;
}

简而言之,问题是这样的:当new_msg_notification.wait(...)被触发时,如何在interrupt中断等待新消息(不引入)超时)?

或者,问题可能理解为:我如何等到两个std::condition_variable中的任何一个发出信号?

一种天真的方法似乎是根本不使用std::condition_variable来代替中断而只是使用原子标记std::atomic<bool> interrupted然后在new_msg_notification上忙着等待很短的时间 - 直到新消息到达或直到true==interrupted为止。但是,我非常希望避免忙碌等待。


编辑:

根据评论和pilcrow的答案,看起来基本上有两种可能的方法。

  1. 按照Alan,mukunda和pilcrow的提议,将特殊的“终止”消息排入队列。我决定反对这个选项,因为在我希望actor终止时我不知道队列的大小。它很可能(因为在我想要快速终止的时候就是这种情况),队列中有数千条消息需要处理,等待它们被处理似乎是不可接受的,直到最终终止消息得到它转动。
  2. 实现条件变量的自定义版本,可以通过将通知转发到第一个线程正在等待的条件变量来中断另一个线程。我选择了这种方法。
  3. 对于那些感兴趣的人,我的实现如下。在我的情况下,条件变量实际上是semaphore(因为我更喜欢它们,因为我喜欢这样做的练习)。我为此信号量配备了相关的interrupt,可以通过semaphore::get_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);
        }
    };
    

3 个答案:

答案 0 :(得分:14)

你问,

  

在C ++ 11中等待多个条件变量的最佳方法是什么?

你不能,而且必须重新设计。一个线程一次只能等待一个条件变量(及其关联的互斥锁)。在这方面,用于同步的Windows设备比那些&#34; POSIX-style&#34;同步原语族。

使用线程安全队列的典型方法是将一个特殊的&#34;所有内容排入队列!&#34;消息,或设计一个&#34;易碎的&#34; (或&#34; shutdown-able&#34;)队列。在后一种情况下,队列的内部条件变量然后保护复杂谓词:项目可用队列已被破坏。

在评论中你会发现

  

如果没有人等待,则notify_all()将无效

这是真的但可能不相关。 wait()条件变量还意味着检查谓词,并在实际阻止通知之前检查。因此,工作线程忙于处理一个&#34;错过&#34; notify_all()下次检查队列条件时,会看到谓词(新项目可用,或队列全部完成)已更改。

答案 1 :(得分:3)

最近,我借助单个条件变量和每个生产者/工人的单独布尔变量解决了这个问题。 消费者线程中的wait函数中的谓词可以检查这些标志,并决定哪个生产者/工人满足条件。

答案 2 :(得分:0)

也许这可行:

摆脱中断。

 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()