【发布时间】: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<bool> interrupted,然后忙于等待new_msg_notification,并有一个非常小的超时,直到收到新消息已经到达或直到true==interrupted。但是,我非常希望避免忙于等待。
编辑:
从 cmets 和 pilcrow 的回答看来,基本上有两种可能的方法。
- 根据 Alan、mukunda 和 pilcrow 的建议,将特殊的“终止”消息排入队列。我决定反对这个选项,因为我不知道在我希望演员终止时队列的大小。很有可能(当我希望快速终止某些事情时,通常是这种情况)队列中有数千条消息要处理,并且等待它们被处理直到最终终止消息得到它似乎是不可接受的转身。
- 实现条件变量的自定义版本,通过将通知转发到第一个线程正在等待的条件变量,可能会被另一个线程中断。我选择了这种方法。
对于那些感兴趣的人,我的实现如下。在我的例子中,条件变量实际上是一个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