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

std::atomic_bool for cancellation flag: is std::memory_order_relaxed the correct memory order?

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

此代码是否正确取决于很多因素。最重要的是,这取决于 "correct" 的确切含义。据我所知,您显示的代码位不会调用未定义的行为(假设您的 work_threadcancel_requested 实际上并未按照您上面的代码片段建议的顺序进行初始化,因为您将拥有线程可能读取原子的未初始化值)。如果您需要做的只是更改该标志的值并让线程最终在某个时刻看到新值,而不管其他可能发生的事情,那么 std::memory_order_relaxed 就足够了。

但是,我看到您的工作线程调用了一个 process_next_element() 函数。这表明工作线程通过某种机制接收要处理的元素。处理完所有元素后,我看不到线程退出的任何方法。当没有立即可用的下一个元素时,process_next_element() 会做什么?它会立即 return 吗?在那种情况下,您将忙于等待更多输入或取消,这会起作用但可能并不理想。或者 process_next_element() 是否在内部调用了一些阻塞直到元素可用的函数!?如果是这种情况,那么取消线程将必须首先设置取消标志,然后执行任何必要的操作以确保下一个元素调用您的线程可能会阻塞 returns。在这种情况下,线程在阻塞调用 return 之后永远看不到取消标志可能是必不可少的。否则,您可能会调用 return,返回循环,仍然读取旧的取消标志,然后再次调用 process_next_element()。如果 process_next_element() 再次保证只是 return,那么你没问题。如果不是这种情况,就会陷入僵局。所以我相信它在技术上取决于 process_next_element() 到底做了什么。 可以 想象一个 process_next_element() 的实现,其中您可能需要的不仅仅是宽松的内存顺序。但是,如果您已经有了获取新元素进行处理的机制,为什么还要使用单独的取消标志呢?您可以通过相同的机制简单地处理取消,例如,通过让它 return 具有特殊值的下一个元素或 return 根本没有元素来表示处理取消并导致线程 return 而不是依赖一个单独的标志…

只要 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() 本身充当两个线程之间的内存屏障。