C++ `std::latch` (C++20):一次性同步栅的原理与应用

好的,没问题。让我们直接开始吧!

各位观众,晚上好!今天我们要聊聊 C++20 中一个非常实用,但可能平时不太引人注意的家伙——std::latch。这家伙就像你家门上的门闩,一次性的,咣当一声关上,放行一批人,之后就永远打开了。

什么是 std::latch?别跟我扯概念,说人话!

std::latch,你可以把它想象成一个倒计时器。一开始,它有一个初始值,代表着需要等待的事件数量。每个事件完成,你就让这个倒计时器减一。当倒计时器归零时,latch 就“打开”了,所有等待的线程就可以继续执行了。注意,是所有 等待的线程,而且 latch 打开后就不能再重置了,这就是“一次性”的含义。

为什么要用 std::latch?难道 std::mutexstd::condition_variable 不香吗?

香,当然香!但是,std::mutexstd::condition_variable 更像是交通信号灯,控制线程对共享资源的访问,保证互斥和同步。而 std::latch 专注于等待多个线程完成初始化或准备工作

举个例子:你想启动一个大型游戏,需要加载地图、模型、音效等资源。这些加载任务可以并行进行。只有当所有资源都加载完毕后,才能开始游戏。这时,std::latch 就派上用场了。

再例如,一个并行计算任务,需要将数据分块,每个线程处理一部分。只有所有线程都处理完毕,才能合并结果。std::latch 也能轻松胜任。

简单来说,std::latch 解决的是等待多个线程到达某个共同点 的问题,而不是控制对共享资源的访问。

std::latch 的基本用法:三板斧

std::latch 的用法非常简单,主要就三个操作:

  1. 构造: 创建 std::latch 对象,并指定初始的计数器值。

  2. count_down() 让计数器减一。通常在每个线程完成任务后调用。

  3. wait() 阻塞当前线程,直到计数器归零。

下面是一个最简单的例子:

#include <iostream>
#include <thread>
#include <latch>

int main() {
  std::latch my_latch(3); // 初始化计数器为 3

  auto worker = [&my_latch](int id) {
    std::cout << "Thread " << id << " is working...n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread " << id << " finished.n";
    my_latch.count_down(); // 计数器减一
  };

  std::thread t1(worker, 1);
  std::thread t2(worker, 2);
  std::thread t3(worker, 3);

  std::cout << "Waiting for all threads to finish...n";
  my_latch.wait(); // 等待计数器归零

  std::cout << "All threads finished! Continuing with main thread.n";

  t1.join();
  t2.join();
  t3.join();

  return 0;
}

在这个例子中,我们创建了一个 std::latch 对象 my_latch,初始值为 3。然后,我们创建了三个线程,每个线程模拟一个工作任务,完成后调用 my_latch.count_down()。主线程调用 my_latch.wait(),阻塞等待,直到三个线程都完成任务,计数器归零,主线程才继续执行。

count_down()arrive_and_wait() 的区别:别晕!

std::latch 还有一个成员函数 arrive_and_wait(),它相当于先调用 count_down(),再调用 wait()。 但是这个函数主要使用在 std::barrier中。std::latch很少用到,可以认为只有 count_down()wait()

std::latch 的高级用法:别只会 Hello World!

光会 Hello World 可不行,我们来点更实际的。

1. 并行数据处理:

假设我们有一个很大的数组,需要并行计算每个元素的平方。

#include <iostream>
#include <vector>
#include <thread>
#include <latch>
#include <numeric> // std::iota

int main() {
  const int array_size = 100000;
  const int num_threads = 4;

  std::vector<int> data(array_size);
  std::iota(data.begin(), data.end(), 1); // 初始化数据为 1, 2, 3, ...

  std::vector<int> results(array_size);

  std::latch my_latch(num_threads);

  auto worker = [&data, &results, &my_latch, array_size, num_threads](int thread_id) {
    int chunk_size = array_size / num_threads;
    int start = thread_id * chunk_size;
    int end = (thread_id == num_threads - 1) ? array_size : start + chunk_size; // 处理最后一个线程的剩余部分

    for (int i = start; i < end; ++i) {
      results[i] = data[i] * data[i];
    }

    my_latch.count_down();
  };

  std::vector<std::thread> threads;
  for (int i = 0; i < num_threads; ++i) {
    threads.emplace_back(worker, i);
  }

  my_latch.wait(); // 等待所有线程完成计算

  std::cout << "All threads finished calculating squares!n";

  // 验证结果(可选)
  // for (int i = 0; i < 10; ++i) {
  //   std::cout << "data[" << i << "] = " << data[i] << ", results[" << i << "] = " << results[i] << "n";
  // }

  for (auto& t : threads) {
    t.join();
  }

  return 0;
}

在这个例子中,我们将数组分成若干个块,每个线程处理一个块。std::latch 用于等待所有线程完成计算。

2. 资源加载:

模拟游戏资源加载的场景。

#include <iostream>
#include <thread>
#include <latch>
#include <vector>

struct Resource {
  std::string name;
  bool loaded = false;
};

void load_resource(Resource& resource) {
  std::cout << "Loading resource: " << resource.name << "n";
  std::this_thread::sleep_for(std::chrono::milliseconds(500 + rand() % 1000)); // 模拟加载时间
  resource.loaded = true;
  std::cout << "Resource loaded: " << resource.name << "n";
}

int main() {
  std::vector<Resource> resources = {
    {"Map", false},
    {"Models", false},
    {"Textures", false},
    {"Audio", false}
  };

  std::latch resource_latch(resources.size());

  auto load_task = [&resources, &resource_latch](int index) {
    load_resource(resources[index]);
    resource_latch.count_down();
  };

  std::vector<std::thread> threads;
  for (size_t i = 0; i < resources.size(); ++i) {
    threads.emplace_back(load_task, i);
  }

  std::cout << "Waiting for all resources to load...n";
  resource_latch.wait();

  std::cout << "All resources loaded! Starting the game.n";

  for (auto& t : threads) {
    t.join();
  }

  return 0;
}

这个例子展示了如何使用 std::latch 等待多个资源加载完成。

std::latchstd::barrier 的区别:双胞胎?

std::latchstd::barrier 都是 C++20 中用于同步线程的工具,但它们的应用场景略有不同。

特性 std::latch std::barrier
用途 等待多个线程完成初始化或准备工作 等待多个线程到达某个共同点,并可以重复使用
重用性 一次性的,计数器归零后无法重置 可以重复使用,每次到达共同点后可以继续下一轮同步
线程数量 初始化时指定,可以少于参与同步的线程数量 初始化时指定,所有线程都必须参与同步
回调函数 可以指定一个回调函数,在所有线程到达共同点后执行

简单来说,std::latch 是一次性的,用于等待初始化或准备工作完成;而 std::barrier 是可重复使用的,用于在循环中同步线程。std::barrier 就像一个田径比赛的起跑线,所有运动员必须到达起跑线才能开始比赛,比赛结束后,可以重新回到起跑线准备下一轮比赛。

std::latch 的注意事项:坑就在你脚下!

  • 计数器不能小于零: 如果 count_down() 的次数超过了初始值,程序会崩溃。这是一个非常常见的错误。

  • 死锁: 如果一个线程在 wait() 之前被取消或抛出异常,可能会导致死锁。确保所有线程最终都会调用 count_down()

  • 异常安全: 如果 count_down() 中抛出异常,可能会导致计数器没有正确递减,从而导致死锁。确保 count_down() 的调用是异常安全的,或者在 catch 块中调用 count_down()

  • 不要和条件变量混淆: std::latch 不是用于保护共享资源的,而是用于同步多个线程的执行。

总结:std::latch 是你的好帮手

std::latch 是 C++20 中一个简单而强大的同步工具。它可以方便地等待多个线程完成初始化、准备工作或计算任务。如果你需要在多个线程之间进行简单的同步,std::latch 绝对是你的好帮手。它比 std::mutexstd::condition_variable 更简单易用,代码也更简洁。

希望今天的讲解对你有所帮助。记住,编程就像盖房子,std::latch 就像一块砖,虽然不起眼,但却是不可或缺的。 掌握好它,你的代码大厦才能更坚固!

现在,大家可以自由提问了。如果没什么问题,我们就下课啦!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注