【问题标题】:What is the best way to wait on multiple condition variables in C++11?在 C++11 中等待多个条件变量的最佳方法是什么?
【发布时间】:2015-02-05 02:34:16
【问题描述】:

首先介绍一下上下文:我正在学习 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) 中使用某种中断机制。

基本上,我要求 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;
}

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

或者,问题可以理解为:我如何等到两个std::condition_variables 中的任何一个收到信号?

一种天真的方法似乎是根本不使用std::condition_variable 进行中断,而是只使用原子标志std::atomic&lt;bool&gt; interrupted,然后忙于等待new_msg_notification,并有一个非常小的超时,直到收到新消息已经到达或直到true==interrupted。但是,我非常希望避免忙于等待。


编辑:

从 cmets 和 pilcrow 的回答看来,基本上有两种可能的方法。

  1. 根据 Alan、mukunda 和 pilcrow 的建议,将特殊的“终止”消息排入队列。我决定反对这个选项,因为我不知道在我希望演员终止时队列的大小。很有可能(当我希望快速终止某些事情时,通常是这种情况)队列中有数千条消息要处理,并且等待它们被处理直到最终终止消息得到它似乎是不可接受的转身。
  2. 实现条件变量的自定义版本,通过将通知转发到第一个线程正在等待的条件变量,可能会被另一个线程中断。我选择了这种方法。

对于那些感兴趣的人,我的实现如下。在我的例子中,条件变量实际上是一个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);
    }
};

【问题讨论】:

  • 向actor发送消息要求其退出?
  • @AlanStokes:是的,这可以工作:) 没想到这种简单的方法。尽管如此,它并没有回答如何等待两个std::condition_variables 中的任何一个发出信号的问题。我相信这一定是可能的,因为它是一个基本的“多生产者,一个消费者”问题。我了解到,在 Windows 下,WaitForMultipleObjects 基本上是我正在寻找的,但我不是在 Windows 上开发,我更喜欢通用的 C++11 解决方案。
  • 我认为你通常最好使用一个条件变量和一个更复杂的关联谓词 - 所以这个变量会告诉你“发生了什么事”,然后你就知道是什么了。
  • 你可能是对的。现在,我对条件变量思考得越多,它们看起来就越没用。首先,我认为它们就像信号量,但现在我意识到,如果没有人在等待,notify_all() 将不起作用。
  • 可以将中断信号作为消息推送吗?

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


【解决方案1】:

你问,

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

你不能,而且必须重新设计。一个线程一次只能等待一个条件变量(及其关联的互斥体)。在这方面,用于同步的 Windows 工具比“POSIX 风格”系列的同步原语更丰富。

使用线程安全队列的典型方法是将特殊的“全部完成!”加入队列。消息,或设计一个“可破坏”(或“可关闭”)队列。在后一种情况下,队列的内部条件变量会保护一个复杂的谓词:要么是一个项目是可用的,要么是队列被破坏了。

在评论中你观察到

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

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 这是从现在开始的 3 年,但有一个错误(为了任何偶然发现此问题的人的利益)。如果您的线程在收到信号时检查 bool[0] 然后 bool[1] ,并且在检查 bool[1] 时, bool[0] 的所有者发送了一个信号,该信号将被错过(因为线程已经唤醒,但它通过了 bool[0] 检查),并且只有在下次有人发送信号时才会执行。
    • @AnkushJain 我没有看到问题;谓词运行时互斥锁被锁定。由于将使用相同的互斥锁来设置任何标志,因此您提到的比赛是不可能的。
    【解决方案3】:

    也许这可行:

    摆脱中断。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-19
      • 2010-11-12
      • 1970-01-01
      • 2010-09-21
      相关资源
      最近更新 更多