简体   繁体   English

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

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

I have a thread that reads from a socket and generates data. 我有一个从套接字读取并生成数据的线程。 After every operation, the thread checks a std::atomic_bool flag to see if it must exit early. 在每次操作之后,线程检查std::atomic_bool标志以查看它是否必须提前退出。

In order to cancel the operation, I set the cancellation flag to true , then call join() on the worker thread object. 为了取消操作,我将取消标志设置为true ,然后在工作线程对象上调用join()

The code of the thread and the cancellation function looks something like this: 线程代码和取消函数看起来像这样:

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

Is std::memory_order_relaxed the correct memory order for this use of an atomic variable? std::memory_order_relaxed是否使用了原子变量的正确内存顺序?

As long as there is no dependency between cancel_requested flag and anything else , you should be safe. 只要在cancel_requested标志和其他任何内容之间没有依赖关系,您就应该是安全的。

The code as shown looks OK, assuming you use cancel_requested only to expedite the shutdown, but also have a provision for an orderly shutdown, such as a sentinel entry in the queue (and of course that the queue itself is synchronized). 如图所示的代码看起来没问题, 假设您仅使用cancel_requested来加速关闭,而且还有一个有序关闭的设置,例如队列中的sentinel条目(当然队列本身是同步的)。

Which means your code actually looks like this: 这意味着您的代码实际上如下所示:

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

And if we're that far, then cancel_requested may just as well become a regular variable, the code even becomes simpler. 如果我们那么远,那么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 is generally hard to reason about, because it blurs the general notion of sequentially executing code. memory_order_relaxed通常很难推理,因为它模糊了顺序执行代码的一般概念。 So the usefulness of it is very, very limited as Herb explains in his atomic weapons talk . 因此,赫伯在他的原子武器谈话中解释说,它的用处非常非常有限。

Note std::thread::join() by itself acts as a memory barrier between the two threads. 注意std::thread::join()本身充当两个线程之间的内存屏障。

Whether this code is correct depends on a lot of things. 这段代码是否正确取决于很多事情。 Most of all it depends on what exactly you mean by "correct". 最重要的是它取决于“正确”的确切含义。 As far as I can tell, the bits of code that you show don't invoke undefined behavior (assuming your work_thread and cancel_requested are not actually initialized in the order your snippet above suggests as you would then have the thread potentially reading the uninitialized value of the atomic). 据我所知,您显示的代码位不会调用未定义的行为(假设您的work_threadcancel_requested实际上没有按照上面的代码段建议的顺序进行初始化,因为您可能会让线程读取未初始化的值。原子)。 If all you need to do is change the value of that flag and have the thread eventually see the new value at some point independent of whatever else may be going on, then std::memory_order_relaxed is sufficient. 如果您需要做的就是更改该标志的值并让线程最终在某个时刻看到新值而不管其他任何可能发生的值,那么std::memory_order_relaxed就足够了。

However, I see that your worker thread calls a process_next_element() function. 但是,我看到你的工作线程调用了process_next_element()函数。 That suggests that there is some mechanism through which the worker thread receives elements to process. 这表明工作线程有一些机制可以接收要处理的元素。 I don't see any way for the thread to exit when all elements have been processed. 在处理完所有元素后,我没有看到线程退出的任何方法。 What does process_next_element() do when there's no next element available right away? 当没有下一个元素可用时, process_next_element()做什么? Does it just return immediately? 它会立即返回吗? In that case you've got yourself a busy wait for more input or cancellation, which will work but is probably not ideal. 在这种情况下,你有一个忙等待更多的输入或取消,这将工作但可能不理想。 Or does process_next_element() internally call some function that blocks until an element becomes available!? 或者process_next_element()内部调用一些阻塞的函数,直到一个元素可用为止!? If that is the case, then cancelling the thread would have to involve first setting the cancellation flag and then doing whatever is needed to make sure the next element call your thread is potentially blocking on returns. 如果是这种情况,那么取消线程必须首先设置取消标志,然后做任何需要确保下一个元素调用你的线程可能阻止返回。 In this case, it's potentially essential that the thread can never see the cancellation flag after the blocking call returns. 在这种情况下,在阻塞调用返回后,线程永远不会看到取消标志是很重要的。 Otherwise, you could potentially have the call return, go back into the loop, still read the old cancellation flag and then go call process_next_element() again. 否则,您可能会返回调用,返回循环,仍然读取旧的取消标志,然后再次调用process_next_element() If process_next_element() is guaranteed to just return again, then you're fine. 如果保证process_next_element()再次返回,那么你没事。 If that is not the case, you have a deadlock. 如果不是这样,那你就陷入了僵局。 So I believe it technically depends on what exactly process_next_element() does. 所以我认为它在技术上取决于process_next_element()确切含义。 One could imagine an implementation of process_next_element() where you would potentially need more than relaxed memory order. 人们可以想象的实现process_next_element()你将有可能需要比宽松内存顺序的更多。 However, if you already have a mechanism for fetching new elements to process, why even use a separate cancellation flag? 但是,如果您已经有一个获取要处理的新元素的机制,为什么甚至使用单独的取消标记? You could simply handle cancellation through that same mechanism, eg, by having it return a next element with a special value or return no element at all to signal cancellation of processing and cause the thread to return instead of relying on a separate flag… 您可以通过相同的机制简单地处理取消,例如,让它返回具有特殊值的下一个元素,或者根本不返回任何元素来表示取消处理并导致线程返回而不是依赖于单独的标志......

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM