C++20并发编程之线程闩(std::latch)和线程卡(std::barrier)

std::latch

std::latch类是一种基于std::ptrdiff_t类型的倒计数器,可用于同步线程。计数器的值在创建时进行初始化。线程可以在 latch 上阻塞,直到计数器减少到零为止。无法增加或重置计数器,这使得 latch 成为一次性的屏障。
std::latch的成员函数的并发调用(除了析构函数)不会引入数据竞争。
与std::barrier不同,std::latch可以被参与的线程多次递减。

主要成员函数

count_down: 这是一个非阻塞的成员函数,用于递减内部计数器。当一个线程完成了它在同步点的工作时,可以调用此函数来告诉std::barrier,一个线程已经到达。这个操作不会阻塞调用线程,因此线程可以继续执行其他任务。

try_wait: 这个成员函数用于测试内部计数器是否已经达到零。如果计数器为零,表示所有线程都已经到达同步点,可以执行后续操作。这是一个非阻塞操作,因为它只是测试计数器的状态而不阻塞调用线程。

wait: 这是一个阻塞的成员函数,用于使调用线程等待,直到内部计数器达到零。当所有线程都已经到达同步点,调用wait的线程将被解除阻塞,可以继续执行后续操作。

arrive_and_wait: 这是一个组合操作,它先递减计数器,然后等待直到计数器达到零。调用线程会递减计数器,然后被阻塞,直到所有线程都到达同步点。这是一个常见的用法,用于确保所有线程在继续执行之前都已经完成了特定的工作。

代码示例

#include 
#include 
#include 
#include 
#include 
 
struct Job
{
    const std::string name;
    std::string product{"not worked"};
    std::thread action{};
};
 
int main()
{
    Job jobs[]{{"Annika"}, {"Buru"}, {"Chuck"}};
 
    std::latch work_done{std::size(jobs)};
    std::latch start_clean_up{1};
 
    auto work = [&](Job& my_job)
    {
        my_job.product = my_job.name + " worked";
        work_done.count_down();
        start_clean_up.wait();
        my_job.product = my_job.name + " cleaned";
    };
 
    std::cout << "Work is starting... ";
    for (auto& job : jobs)
        job.action = std::thread{work, std::ref(job)};
 
    work_done.wait();
    std::cout << "done:\n";
    for (auto const& job : jobs)
        std::cout << "  " << job.product << '\n';
 
    std::cout << "Workers are cleaning up... ";
    start_clean_up.count_down();
    for (auto& job : jobs)
        job.action.join();
 
    std::cout << "done:\n";
    for (auto const& job : jobs)
        std::cout << "  " << job.product << '\n';
}

std::latch 的特点:

一次性的:std::latch 是一次性的,一旦计数器减至零,无法重新使用。如果需要可以多次使用的机制,可以考虑 std::barrier。

线程安全:std::latch 是线程安全的,多个线程可以同时调用 count_down 和 wait。

无超时等待:std::latch 的 wait 函数没有提供超时参数,如果需要超时等待,可以使用 std::barrier 或其他机制。

std::barrier

std::barrier类模板提供了一种线程协调机制,它阻塞了一个已知大小的线程组,直到该组中的所有线程都到达屏障为止。与std::latch不同,屏障是可重用的:一旦一组到达的线程被解除阻塞,就可以重新使用该屏障。与std::latch不同,屏障在解除线程阻塞之前执行一个可能为空的可调用对象。

屏障对象的生命周期由一个或多个阶段组成。每个阶段定义了一个阶段同步点,在该点等待的线程将被阻塞。线程可以到达屏障,但通过调用arrive可以延迟在阶段同步点上等待。这样的线程稍后可以通过调用wait在阶段同步点上阻塞。

主要成员函数

arrive: 这是 std::barrier 的一个成员函数,用于将线程到达栅栏并减少预期计数。当线程调用 arrive 时,预期计数会减少。一旦所有线程都到达这个栅栏,预期计数归零,栅栏会打开,允许所有线程继续执行。

wait: 这个成员函数使线程在阶段同步点阻塞,直到当前阶段的完成步骤运行。一旦线程调用 wait 并在同步点阻塞,它将一直等待直到当前阶段完成,然后才能继续执行。

arrive_and_wait: 这个成员函数结合了前两者的功能。它使线程到达栅栏并将预期计数减少一,然后在同步点阻塞,直到当前阶段完成。与简单的 arrive 不同,arrive_and_wait 在到达后会立即阻塞,直到当前阶段完成为止。

arrive_and_drop: 这个成员函数不仅减少当前阶段的预期计数,还会减少后续阶段的初始预期计数。这意味着它不仅影响当前阶段,还会影响未来阶段。这对于在某些情况下提前放弃等待是有用的,因为它不仅影响当前阶段的计数,还影响到后续的阶段。

代码示例

#include 
#include 
#include 
#include 
#include 
#include 
 
int main()
{
    const auto workers = {"Anil", "Busara", "Carl"};
 
    auto on_completion = []() noexcept
    {
        // locking not needed here
        static auto phase =
            "... done\n"
            "Cleaning up...\n";
        std::cout << phase;
        phase = "... done\n";
    };
 
    std::barrier sync_point(std::ssize(workers), on_completion);
 
    auto work = [&](std::string name)
    {
        std::string product = "  " + name + " worked\n";
        std::osyncstream(std::cout) << product;  // ok, op<< call is atomic
        sync_point.arrive_and_wait();
 
        product = "  " + name + " cleaned\n";
        std::osyncstream(std::cout) << product;
        sync_point.arrive_and_wait();
    };
 
    std::cout << "Starting...\n";
    std::vector<std::jthread> threads;
    threads.reserve(std::size(workers));
    for (auto const& worker : workers)
        threads.emplace_back(work, worker);
}

主要特点

线程同步: 提供了一种机制,可以阻塞一组线程,直到所有线程都达到了某个同步点。这有助于协调并发执行的线程,确保它们在特定点同步执行或释放。

可重用: 与 std::latch 不同,std::barrier 是可重用的。一旦一组线程被释放,栅栏可以被重新使用,允许线程再次聚集在同一点。

阶段性同步: std::barrier 可以分为多个阶段,每个阶段定义了一个同步点。线程可以在同步点前到达,并在需要时等待。这使得可以在多个同步点执行特定的操作。

可定制的同步操作: 提供了一个回调函数,被称为 CompletionFunction,可以在所有线程到达同步点后执行。这个函数可以用来执行一些特定的操作,例如修改共享数据结构或执行其他同步操作。

多种成员函数: std::barrier 提供了不同的成员函数,如 arrive、wait、arrive_and_wait 和 arrive_and_drop,使得线程可以以不同的方式到达同步点并执行不同的同步操作。

线程安全: 除了析构函数外,std::barrier 的成员函数的并发调用不会引入数据竞争。这意味着多个线程可以安全地调用 std::barrier 的成员函数,而不需要额外的同步措施。

你可能感兴趣的:(c++20)