我最近听说过新的c ++标准功能:
我无法弄明白,在哪种情况下,它们适用于彼此并且非常有用。
答案 0 :(得分:6)
它们实际上是针对完全不同的目标
当您具有执行某些处理的工作线程池以及在它们之间共享的工作项队列时,通常使用屏障和闩锁。这不是使用它们的唯一情况,但这是一种非常常见的情况,确实有助于说明它们之间的差异。这是一些示例代码,它们可以设置如下这样的线程:
const size_t worker_count = 7; // or whatever
std::vector<std::thread> workers;
std::vector<Proc> procs(worker_count);
Queue<std::function<void(Proc&)>> queue;
for (size_t i = 0; i < worker_count; ++i) {
workers.push_back(std::thread(
[p = &procs[i], &queue]() {
while (auto fn = queue.pop_back()) {
fn(*p);
}
}
));
}
在该示例中,我假设存在两种类型:
Proc
:特定于您的应用程序的类型,包含处理工作项所必需的数据和逻辑。对一个引用将传递给线程池中运行的每个回调函数。Queue
:线程安全的阻塞队列。 C ++标准库中没有这样的东西(有点令人惊讶),但是有很多包含它们的开源库。 Folly MPMCQueue
或moodycamel::ConcurrentQueue
,或者您可以使用std::mutex
,std::condition_variable
和std::deque
自己打造一个不太喜欢的人(如果有很多示例,您是Google的用户。闩锁通常用于等待您推送到队列中的某些工作项全部完成,以便您可以检查结果。
std::vector<WorkItem> work = get_work();
std::latch latch(work.size());
for (WorkItem& work_item : work) {
queue.push_back([&work_item, &latch](Proc& proc) {
proc.do_work(work_item);
latch.count_down();
});
}
latch.wait();
// Inspect the completed work
工作原理:
latch.count_down()
,以有效地减少从work.size()
开始的内部计数器。latch.wait()
返回,并且生产者线程知道所有工作项都已处理。注释:
count_down()
方法可能被称为零次,一次或多次,并且该数字对于不同的线程可能有所不同。例如,即使您将7条消息推送到7个线程上,也可能是所有7个项目都在同一个线程上处理(而不是每个线程一个),这很好。latch.wait()
才可能被调用。 (这是编写线程代码时需要注意的一种奇怪的情况。)但是,这不是竞争条件:latch.wait()
在这种情况下将立即返回。queue
相反。这也是一种完全有效的策略,实际上,如果有任何更常见的策略,但是在其他情况下,闩锁更有用。通常使用屏障来使所有线程同时等待,以便可以同时操作与所有线程关联的数据。
typedef Fn std::function<void()>;
Fn completionFn = [&procs]() {
// Do something with the whole vector of Proc objects
};
auto barrier = std::make_shared<std::barrier<Fn>>(worker_count, completionFn);
auto workerFn = [barrier](Proc&) {
barrier->count_down_and_wait();
};
for (size_t i = 0; i < worker_count; ++i) {
queue.push_back(workerFn);
}
工作原理:
workerFn
项目之一,并调用barrier.count_down_and_wait()
。completionFn()
,而其他人会继续等待。count_down_and_wait()
返回,并可以从队列中弹出其他不相关的工作项。注释:
workerFn
并进行处理。一旦某个线程从队列中弹出一个,它将在barrier.count_down_and_wait()
中等待,直到workerFn
的所有其他副本都被其他线程弹出,因此没有机会弹出另一个线程。latch.wait()
)。生产者线程在这里不需要等待障碍,因此我们需要以其他方式管理内存。count_down_and_wait()
,但是显然您需要将worker_count + 1
传递给屏障的构造函数。 (然后,您无需为障碍使用共享指针。)!!!危险!!!
只有在其他工作也没有使用障碍的情况下,关于其他工作才被“罚款”的最后一个要点是!如果您有两个不同的生产者线程,将带有屏障的工作项放在同一队列中,并且这些项目是交错的,则某些线程将在一个屏障上等待,而其他线程在另一个屏障上等待,而任何线程都将达不到所需的等待计数-< strong> DEADLOCK 。避免这种情况的一种方法是仅在单个线程中使用这种屏障,或者甚至在整个程序中仅使用一种屏障(这听起来很极端,但实际上是相当普遍的策略,因为屏障通常用于启动时间初始化)。如果您正在使用的线程队列支持该方法,则另一种选择是将所有用于屏障的工作项立即自动推送到队列中,这样它们就不会与任何其他工作项交错。 (这不适用于moodycamel
队列,该队列支持一次推送多个项目,但不能保证它们不会与其他线程推送的项目相互关联。)
在您问这个问题时,建议的实验性API不支持完成功能。即使当前的API至少也不允许使用它们,所以我认为我应该展示一个示例,说明如何也可以像这样使用障碍。
auto barrier = std::make_shared<std::barrier<>>(worker_count);
auto workerMainFn = [&procs, barrier](Proc&) {
barrier->count_down_and_wait();
// Do something with the whole vector of Proc objects
barrier->count_down_and_wait();
};
auto workerOtherFn = [barrier](Proc&) {
barrier->count_down_and_wait(); // Wait for work to start
barrier->count_down_and_wait(); // Wait for work to finish
}
queue.push_back(std::move(workerMainFn));
for (size_t i = 0; i < worker_count - 1; ++i) {
queue.push_back(workerOtherFn);
}
工作原理:
关键思想是在每个线程中等待两次屏障,然后在两者之间进行工作。第一次等待与前面的示例具有相同的目的:它们确保在开始此工作之前,队列中所有较早的工作项都已完成。第二次等待确保队列中的所有后续项目在此工作完成之前不会开始。
注释:
注释与前面的障碍示例大致相同,但有一些区别:
count_down()
和wait()
来代替count_down_and_wait()
。但是使用屏障更有意义,这既是因为调用组合函数更简单,又是因为使用屏障将您的意图更好地传达给了将来的代码读者。