Bud*_*nga 5 c++ std visual-c++ c++-experimental
我最近听说了新的c ++标准功能,这些功能包括:
我无法弄清楚它们在哪些情况下比另一种情况更适用和有用。
Art*_*cca 10
它们确实针对完全不同的目标:
当您有一个执行某些处理的工作线程池和一个在它们之间共享的工作项队列时,通常会使用屏障和闩锁。这不是使用它们的唯一情况,但它是一种非常常见的情况,确实有助于说明差异。下面是一些示例代码,可以像这样设置一些线程:
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);
}
}
));
}
Run Code Online (Sandbox Code Playgroud)
我假设在该示例中存在两种类型:
Proc:特定于您的应用程序的类型,包含处理工作项所需的数据和逻辑。对一个的引用传递给线程池中运行的每个回调函数。Queue: 一个线程安全的阻塞队列。C++ 标准库中没有这样的东西(有点令人惊讶)但是有很多包含它们的开源库,例如FollyMPMCQueue或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
Run Code Online (Sandbox Code Playgroud)
这是如何工作的:
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);
}
Run Code Online (Sandbox Code Playgroud)
这是如何工作的:
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给屏障的构造函数。(然后你就不需要为屏障使用共享指针了。)!!!危险 !!!
关于其他工作被推入队列的最后一个要点是“很好”,只有当其他工作也没有使用障碍时才会出现这种情况!如果您有两个不同的生产者线程将带有屏障的工作项放在同一个队列中,并且这些项目是交错的,那么一些线程将在一个屏障上等待而其他线程在另一个屏障上等待,并且都不会达到所需的等待计数 -死锁. 避免这种情况的一种方法是只在单个线程中使用这样的屏障,或者甚至在整个程序中只使用一个屏障(这听起来很极端,但实际上是一种很常见的策略,因为屏障通常用于一个-启动时的时间初始化)。另一种选择是,如果您使用的线程队列支持它,则立即将屏障的所有工作项以原子方式推送到队列中,这样它们就不会与任何其他工作项交错。(这不适用于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);
}
Run Code Online (Sandbox Code Playgroud)
这是如何工作的:
关键思想是在每个线程中等待屏障两次,并在两者之间进行工作。第一个等待与前面的示例具有相同的目的:它们确保队列中的任何较早的工作项在开始此工作之前已完成。第二次等待确保队列中的任何后续项目在此工作完成之前不会启动。
笔记:
注释与前面的障碍示例大致相同,但有一些不同之处:
count_down()然后wait()代替count_down_and_wait(). 但是使用屏障更有意义,因为调用组合函数更简单,而且使用屏障可以更好地向未来的代码读者传达您的意图。