std :: atomic_bool用于取消标志:std :: memory_order_relaxed是否为正确的内存顺序?

时间:2018-12-06 14:10:01

标签: c++ atomic cancellation memory-barriers relaxed-atomics

我有一个从套接字读取并生成数据的线程。每次操作后,线程都会检查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是否正确的内存顺序?

2 个答案:

答案 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_threadcancel_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()的实现,在那里您可能需要的不仅仅是放松的存储顺序。但是,如果您已经有了一种获取新元素以进行处理的机制,为什么还要使用单独的取消标志呢?您可以通过相同的机制简单地处理取消操作,例如,让它返回具有特殊值的下一个元素,或者根本不返回任何元素,以表示取消处理并导致线程返回而不是依赖于单独的标志……