好的,没问题。让我们直接开始吧!
各位观众,晚上好!今天我们要聊聊 C++20 中一个非常实用,但可能平时不太引人注意的家伙——std::latch
。这家伙就像你家门上的门闩,一次性的,咣当一声关上,放行一批人,之后就永远打开了。
什么是 std::latch
?别跟我扯概念,说人话!
std::latch
,你可以把它想象成一个倒计时器。一开始,它有一个初始值,代表着需要等待的事件数量。每个事件完成,你就让这个倒计时器减一。当倒计时器归零时,latch
就“打开”了,所有等待的线程就可以继续执行了。注意,是所有 等待的线程,而且 latch
打开后就不能再重置了,这就是“一次性”的含义。
为什么要用 std::latch
?难道 std::mutex
和 std::condition_variable
不香吗?
香,当然香!但是,std::mutex
和 std::condition_variable
更像是交通信号灯,控制线程对共享资源的访问,保证互斥和同步。而 std::latch
专注于等待多个线程完成初始化或准备工作。
举个例子:你想启动一个大型游戏,需要加载地图、模型、音效等资源。这些加载任务可以并行进行。只有当所有资源都加载完毕后,才能开始游戏。这时,std::latch
就派上用场了。
再例如,一个并行计算任务,需要将数据分块,每个线程处理一部分。只有所有线程都处理完毕,才能合并结果。std::latch
也能轻松胜任。
简单来说,std::latch
解决的是等待多个线程到达某个共同点 的问题,而不是控制对共享资源的访问。
std::latch
的基本用法:三板斧
std::latch
的用法非常简单,主要就三个操作:
-
构造: 创建
std::latch
对象,并指定初始的计数器值。 -
count_down()
: 让计数器减一。通常在每个线程完成任务后调用。 -
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::latch
和 std::barrier
的区别:双胞胎?
std::latch
和 std::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::mutex
和 std::condition_variable
更简单易用,代码也更简洁。
希望今天的讲解对你有所帮助。记住,编程就像盖房子,std::latch
就像一块砖,虽然不起眼,但却是不可或缺的。 掌握好它,你的代码大厦才能更坚固!
现在,大家可以自由提问了。如果没什么问题,我们就下课啦!