我有一个从套接字读取并生成数据的线程。每次操作后,线程都会检查std::atomic_bool
标志以查看是否必须提前退出。
为了取消操作,我将取消标志设置为true
,然后在辅助线程对象上调用join()
。
线程和取消函数的代码如下所示:
std::thread work_thread;
std::atomic_bool cancel_requested{false};
void thread_func()
{
while(! cancel_requested.load(std::memory_order_relaxed))
process_next_element();
}
void cancel()
{
cancel_requested.store(true, std::memory_order_relaxed);
work_thread.join();
}
此原子变量的使用std::memory_order_relaxed
是否正确的内存顺序?
答案 0 :(得分:3)
只要cancel_requested
标志与其他任何东西之间没有依赖性,您就应该安全。
如下所示的代码看起来不错,假设,您使用cancel_requested
只是为了加快关机速度,而且还提供了有序关机的条件,例如队列中的哨兵条目(当然队列本身是同步的。
这意味着您的代码实际上看起来像这样:
std::thread work_thread;
std::atomic_bool cancel_requested{false};
std::mutex work_queue_mutex;
std::condition_variable work_queue_filled_cond;
std::queue work_queue;
void thread_func()
{
while(! cancel_requested.load(std::memory_order_relaxed))
{
std::unique_lock<std::mutex> lock(work_queue_mutex);
work_queue_filled_cond.wait(lock, []{ return !work_queue.empty(); });
auto element = work_queue.front();
work_queue.pop();
lock.unlock();
if (element == exit_sentinel)
break;
process_next_element(element);
}
}
void cancel()
{
std::unique_lock<std::mutex> lock(work_queue_mutex);
work_queue.push_back(exit_sentinel);
work_queue_filled_cond.notify_one();
lock.unlock();
cancel_requested.store(true, std::memory_order_relaxed);
work_thread.join();
}
如果我们走得那么远,那么cancel_requested
可能也成为常规变量,代码甚至变得更简单。
std::thread work_thread;
bool cancel_requested = false;
std::mutex work_queue_mutex;
std::condition_variable work_queue_filled_cond;
std::queue work_queue;
void thread_func()
{
while(true)
{
std::unique_lock<std::mutex> lock(work_queue_mutex);
work_queue_filled_cond.wait(lock, []{ return cancel_requested || !work_queue.empty(); });
if (cancel_requested)
break;
auto element = work_queue.front();
work_queue.pop();
lock.unlock();
process_next_element(element);
}
}
void cancel()
{
std::unique_lock<std::mutex> lock(work_queue_mutex);
cancel_requested = true;
work_queue_filled_cond.notify_one();
lock.unlock();
work_thread.join();
}
memory_order_relaxed
通常很难推理,因为它模糊了顺序执行代码的一般概念。因此,正如Herb在他的atomic weapons talk中解释的那样,它的用途非常非常有限。
注意std::thread::join()
本身是两个线程之间的内存屏障。
答案 1 :(得分:2)
此代码是否正确取决于很多事情。最重要的是取决于“正确”的确切含义。据我所知,您显示的代码不会调用未定义的行为(假设您的work_thread
和cancel_requested
实际上并未按照上面的摘录建议的顺序进行初始化,因为这样您就可以线程可能会读取原子的未初始化值)。如果您要做的就是更改该标志的值,并让线程最终在某个时刻看到新值而与其他情况无关,那么std::memory_order_relaxed
就足够了。
但是,我看到您的工作线程调用了process_next_element()
函数。这表明存在某种机制,工作线程通过该机制接收要处理的元素。处理完所有元素后,我看不到线程退出的任何方式。当没有下一个可用元素时,process_next_element()
会做什么?它会立即返回吗?在这种情况下,您会忙于等待更多的输入或取消,这虽然可行,但可能并不理想。还是process_next_element()
在内部调用某个阻塞的函数,直到某个元素可用!!如果是这种情况,那么取消线程将必须先设置取消标志,然后再进行所有必要的操作,以确保线程的下一个元素调用可能在返回时阻塞。在这种情况下,潜在的必要条件是阻塞调用返回后线程永远看不到取消标志。否则,您可能会返回呼叫,回到循环中,仍然读取旧的取消标志,然后再次呼叫process_next_element()
。如果保证process_next_element()
会再次返回,那么您就可以了。否则,您将陷入僵局。因此,我认为从技术上来说,这取决于process_next_element()
的确切功能。一个可以想象一种process_next_element()
的实现,在那里您可能需要的不仅仅是放松的存储顺序。但是,如果您已经有了一种获取新元素以进行处理的机制,为什么还要使用单独的取消标志呢?您可以通过相同的机制简单地处理取消操作,例如,让它返回具有特殊值的下一个元素,或者根本不返回任何元素,以表示取消处理并导致线程返回而不是依赖于单独的标志……>