声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
总结整理 POSIX 和 C++ 提供的线程同步技术,包括:条件变量、信号量、future、闩锁(Latch)和 屏障(Barrier),介绍相关概念、接口和使用示例,并做简单的技术对比。
目录
1 基本概念
线程同步:线程之间需要分工协作,一个线程在它运行的过程中,可能需要停下来,等待另一个线程完成某个特定任务,然后才能继续运行。
为实现上述目标,我们可以利用“共享数据”存储一个类似“任务完成”的标志,由“被等待的线程”更新该标志,而“等待的线程”则不断检查该标志。
bool done_flag;
std::mutex m;
void wait_for_flag() { // 通过不断检查标志,等待任务完成
std::unique_lock<std::mutex> lk(m);
while (!done_flag) {
lk.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
lk.lock();
}
}
但是,上述方法并不理想:
- 不断检查标志,会浪费处理器的时间;
- 每次检查标志,都要通过互斥施加保护,会影响其它线程的运行;
- 在两次检查之间进行休眠,
- 如果休眠期太短,线程会频繁检查,虚耗处理时间;
- 如果休眠期太长,线程会因为“不能及时”获得任务完成的信息,而导致处理延迟。
更好的同步机制是:
对于一个线程 A,如果需要在某个条件成立的情况下,才能继续往下执行,那么:
- 当条件不满足时,线程 A 阻塞等待,让出处理器供其他线程运行;
- 当条件满足时,其他线程向线程 A 发送通知信号,线程 A 被唤醒,继续运行。
2 基于 POSIX 的线程同步
2.1 条件变量
条件变量(Condition Variable)是一种同步机制,基本操作包括:
- 等待条件(wait for the condition)
当条件不满足时,线程挂起执行,让出处理器,直到其它线程发出条件信号; - 发出信号(signal the condition)
当条件满足时,另一个线程发送条件成立信号,唤醒挂起的线程; - 条件变量必须与互斥锁关联,以避免竞争条件,使用示意如下:
- 线程1,首先加锁,如果条件不满足,则“原子地”解锁并挂起等待;(通过原子操作,可以确保在条件不满足的情况下进行挂起)
- 线程2,尝试加锁,在线程1“加锁前”或“解锁并挂起”后,加锁成功,接着促使条件满足,[解锁位置1,如果不需要精确的调度控制],然后发出条件信号,[解锁位置2,在条件满足的情况下、在互斥锁的保护下发出信号];
- 线程1,被唤醒并重新加锁,进行相关处理,最后解锁。
- 上述机制确保:在一个线程(判断条件不满足)准备等待条件变量时,另一个线程不会在它实际等待(解锁并挂起)之前发出条件信号,从而避免了信号丢失的情况(即,避免了在条件已满足的情况下等待)。
上述线程交互的时序示意:
线程1解锁后 线程2加锁时序 | 线程1 (等待线程) | 线程2 (通知线程) | 线程1加锁前 线程2加锁时序 | 线程1 (等待线程) | 线程2 (通知线程) | |
---|---|---|---|---|---|---|
1 | 加锁成功 | 1 | 加锁成功 | |||
2 | 判断条件不满足 | 尝试加锁 | 2 | 尝试加锁 | 使条件满足 | |
3 | 解锁并挂起等待 | 3 | 解锁 [位置1] | |||
4 | 加锁成功 | 4 | 加锁成功 | 发送信号(被忽略) | ||
5 | 使条件满足 | 5 | 判断条件满足 | |||
6 | 解锁 [位置1] | 6 | 相关操作 | |||
7 | 发送信号 | 7 | 解锁 | |||
8 | 唤醒并加锁 | |||||
9 | 相关操作 | |||||
10 | 解锁 |
2.1.1 常用 API 函数
序号 | 函数 | 说明 |
---|---|---|
1 | int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); | 按照 attr 属性(可以为 NULL ,即默认属性)初始化条件变量 cond ;不再使用时,需要通过 pthread_cond_destroy 销毁。 |
2 | int pthread_cond_destroy(pthread_cond_t *cond); | 销毁条件变量 cond (不能正在被使用,如线程阻塞等待) |
3 | pthread_cond_t cond = PTHREAD_COND_INITIALIZER; | 以静态方式初始化条件变量,使用 PTHREAD_COND_INITIALIZER (默认属性) 直接给条件变量赋值,不需要销毁。 |
4 | int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); | 对 mutex 解锁,同时(原子地)阻塞等待条件变量 cond ;在调用函数前,应确保当前线程已对 mutex 加锁;在函数返回时,当前线程已完成对 mutex 的再次加锁;因为可能会发生虚假唤醒,所以在函数返回后,应该判断线程是否需要“继续阻塞,等待下一次唤醒”。 |
5 | int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); | 同 pthread_cond_wait() ,除外:如果在时间 abstime 到达前,没有收到条件信号,函数将返回错误码 ETIMEDOUT ,同时对 mutex 加锁。 |
6 | int pthread_cond_signal(pthread_cond_t *cond); | 将阻塞等待条件变量 cond 的“一个”线程解除阻塞;如果没有线程在条件变量上阻塞,函数调用没有任何效果; 如果有多个线程在条件变量上阻塞,将根据调度策略决定线程解除阻塞的顺序; 需要确保与条件变量关联的互斥锁及时解锁,以使 pthread_cond_wait/timedwait() 能够及时加锁并返回。 |
7 | int pthread_cond_broadcast(pthread_cond_t *cond); | 将阻塞等待条件变量 cond 的“所有”线程解除阻塞;如果有多个线程被解除阻塞,它们将根据调度策略(如果适用)争夺互斥锁,就好像每个线程都调用了 pthread_mutex_lock() 。 |
关于返回值:
- 如果成功,函数均返回
0
; - 在内存等资源不足、参数错误等情况下,会返回相应错误码。
通过 man7.org/linux/man-pages 搜索 pthread_cond
可以查看更多相关函数,以及更详细的说明。
2.1.2 使用示例
生产者和消费者问题(Producer and Consumer Problem)
生产者和消费者问题是并发编程中少数几个标准且著名的问题之一。它涉及一个有限大小的缓冲区和两类线程:生产者线程、消费者线程。生产者将元素放入缓冲区,消费者从缓冲区取出元素。在缓冲区有可用空间之前,生产者不能向缓冲区放入任何元素;在生产者写入缓冲区之前,消费者不能从缓冲区取出任何元素。
对于生产者和消费者问题,条件变量的使用示例:
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
#define PRODUCER_COUNT 2 // 多个生产者
#define MAX_ITEM_COUNT 10 // 处理元素的上限,生产/消费完毕后即退出
#define BUF_SIZE 2 // 缓冲区容量
typedef struct {
char buf[BUF_SIZE]; // 缓冲区
int occupied; // 缓存区中元素的个数(已被占用的槽的个数)
int nextin; // 下一个放入元素的位置
int nextout; // 下一个取出元素的位置
pthread_mutex_t mutex; // 互斥锁,保护缓冲区的访问
pthread_cond_t more; // 向缓冲区放入(增加 more)元素的条件变量
pthread_cond_t less; // 从缓冲区取出(减少 less)元素的条件变量
} buffer_t;
buffer_t buffer;
// 生产者将元素放入缓冲区
void insert_item(buffer_t *buf, char item, int id) {
pthread_mutex_lock(&buf->mutex); // 获取缓冲区的互斥锁,以确保对缓冲区的独占访问
while (buf->occupied >= BUF_SIZE) { // 如果缓冲区已满,等待消费者释放空间
pthread_cond_wait(&buf->less, &buf->mutex);
}
buf->buf[buf->nextin++] = item; // 将元素放入缓冲区,更新下一个放入位置
buf->nextin %= BUF_SIZE;
buf->occupied++; // 增加缓冲区中元素的个数
// 如果 b->occupied < BUF_SIZE,b->nextin 是缓冲区中下一个空槽的索引
// 如果 b->occupied == BUF_SIZE,b->nextin 是下一个将被消费者清空的槽的索引
printf("Producer[%d] insert: %c\n", id, item);
pthread_cond_signal(&buf->more); // 通知消费者有元素放入缓冲区
pthread_mutex_unlock(&buf->mutex); // 释放缓冲区的互斥锁
}
// 消费者从缓冲区中取出元素
char remove_item(buffer_t *buf) {
char item;
pthread_mutex_lock(&buf->mutex); // 获取缓冲区的互斥锁,以确保对缓冲区的独占访问
while (buf->occupied <= 0) { // 如果缓冲区为空,等待生产者放入元素
pthread_cond_wait(&buf->more, &buf->mutex);
}
item = buf->buf[buf->nextout++]; // 从缓冲区中取出元素,更新下一个取出位置
buf->nextout %= BUF_SIZE;
buf->occupied--; // 减少缓冲区中元素的个数
// 如果 b->occupied > 0,b->nextout 是缓冲区中下一个元素的索引
// 如果 b->occupied == 0,b->nextout 是下一个将被生产者填充的槽的索引
printf("Consumer remove: %c\n", item);
pthread_cond_signal(&buf->less); // 通知生产者有元素被取出
pthread_mutex_unlock(&buf->mutex); // 释放缓冲区的互斥锁
return item;
}
// 生产者线程函数
void *producer_proc(void *arg) {
int id = (int)(long)arg;
for (int i = id; i < MAX_ITEM_COUNT; i += PRODUCER_COUNT) { // 每个线程各自生产指定编号的数据
char item = '0' + (i % 10);
usleep(1000); // 模拟生产时间
insert_item(&buffer, item, id); // 将元素放入缓冲区
}
return NULL;
}
// 消费者线程函数
void *consumer_proc(void *arg) {
for (int item_idx = 0; item_idx < MAX_ITEM_COUNT; item_idx++) {
char item = remove_item(&buffer); // 从缓冲区中取出元素
usleep(1000); // 模拟消费时间
}
return NULL;
}
void init_buffer(buffer_t *buf) {
int ret;
buf->occupied = 0;
buf->nextin = 0;
buf->nextout = 0;
if (ret = pthread_mutex_init(&buf->mutex, NULL)) handle_error_en(ret, "mutex init");
if (ret = pthread_cond_init(&buf->more, NULL)) handle_error_en(ret, "producer_cond init");
if (ret = pthread_cond_init(&buf->less, NULL)) handle_error_en(ret, "consumer_cond init");
}
void uninit_buffer(buffer_t *buf) {
int ret;
if (ret = pthread_mutex_destroy(&buf->mutex)) handle_error_en(ret, "mutex destroy");
if (ret = pthread_cond_destroy(&buf->more)) handle_error_en(ret, "producer_cond destroy");
if (ret = pthread_cond_destroy(&buf->less)) handle_error_en(ret, "consumer_cond destroy");
}
int main() {
int ret;
pthread_t consumer_id;
pthread_t producer_id[PRODUCER_COUNT];
init_buffer(&buffer);
if (ret = pthread_create(&consumer_id, NULL, consumer_proc, NULL)) handle_error_en(ret, "consumer create");
for (int i = 0; i < PRODUCER_COUNT; i++) {
if (ret = pthread_create(&producer_id[i], NULL, producer_proc, (void *)(long)i)) handle_error_en(ret, "producer create");
}
if (ret = pthread_join(consumer_id, NULL)) handle_error_en(ret, "consumer join");
for (int i = 0; i < PRODUCER_COUNT; i++) {
if (ret = pthread_join(producer_id[i], NULL)) handle_error_en(ret, "producer join");
}
uninit_buffer(&buffer);
return 0;
}
2.2 信号量
在概念上,可以将信号量看做是一个非负整数计数,通常用于协调对共享资源的访问。
- 将信号量计数初始化为可用资源的数量;
- 当资源被添加时,线程会原子地增加计数;
- 当资源被移除时,线程会原子地减少计数;
- 当信号量计数变为零时,表示没有更多的资源可用;此时,如果线程尝试减少信号量将会阻塞,直到计数变得大于零。
信号量既可以用于线程同步,又可以用于线程互斥,复习回顾 线程池2-线程互斥 => 2.3 信号量。
在线程同步场景,信号量与条件变量比较:
- 条件变量需要与互斥锁一起使用,而信号量可以独立使用;
- 在信号是否会丢失方面:
- 对于条件变量,在发出信号时,如果没有线程在条件变量上阻塞,则信号没有任何效果(信号会丢失);
- 对于信号量,在发出信号时,即使没有线程在信号量上阻塞,计数也会增加(信号不会丢失)。
2.2.1 常用 API 函数
复习回顾 线程池2-线程互斥 => 2.3 信号量。
2.2.2 使用示例
修改 条件变量示例 中的 buffer_t
定义和元素存取函数 insert_item()
、remove_item()
:
#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
#define PRODUCER_COUNT 2 // 多个生产者
#define MAX_ITEM_COUNT 10 // 处理元素的上限,生产/消费完毕后即退出
#define BUF_SIZE 2 // 缓冲区容量
typedef struct {
char buf[BUF_SIZE]; // 缓冲区
sem_t occupied_sem; // 缓存区中元素的个数(已被占用的槽的个数)
sem_t empty_sem; // 缓存区中空槽的个数
int nextin; // 下一个放入元素的位置
int nextout; // 下一个取出元素的位置
sem_t in_mtx; // 二值信号量,多个生产者不能同时放入元素(可换用互斥锁)
sem_t out_mtx; // 二值信号量,多个消费者不能同时取出元素(可换用互斥锁)
} buffer_t;
buffer_t buffer;
// 生产者将元素放入缓冲区
void insert_item(buffer_t *buf, char item, int id) {
while (sem_wait(&buf->empty_sem) == -1) { // 空槽个数信号量减 1,如果当前值不大于 0,则挂起等待消费者取出元素
continue; // 被信号中断后,重新执行 sem_wait
}
while (sem_wait(&buf->in_mtx) == -1) { // 二值信号量减 1(加锁)
continue;
}
buf->buf[buf->nextin++] = item; // 将元素放入缓冲区,更新下一个放入位置
buf->nextin %= BUF_SIZE;
printf("Producer[%d] insert: %c\n", id, item);
sem_post(&buf->in_mtx); // 二值信号量加 1(解锁)
sem_post(&buf->occupied_sem); // 元素个数信号量加 1
}
// 消费者从缓冲区中取出元素
char remove_item(buffer_t *buf) {
char item;
while (sem_wait(&buf->occupied_sem) == -1) { // 元素个数信号量减 1,如果当前值不大于 0,则挂起等待生产者放入元素
continue; // 被信号中断后,重新执行 sem_wait
}
while (sem_wait(&buf->out_mtx) == -1) { // 二值信号量减 1(加锁)
continue;
}
item = buf->buf[buf->nextout++]; // 从缓冲区中取出元素,更新下一个取出位置
buf->nextout %= BUF_SIZE;
printf("Consumer remove: %c\n", item);
sem_post(&buf->out_mtx); // 二值信号量加 1(解锁)
sem_post(&buf->empty_sem); // 空槽个数信号量加 1
return item;
}
void init_buffer(buffer_t *buf) {
buf->nextin = 0;
buf->nextout = 0;
// 初始化 counting semaphore,元素个数为 0,空槽个数为 BUF_SIZE
if (sem_init(&buf->occupied_sem, 0, 0)) handle_error_en(errno, "occupied sem_init");
if (sem_init(&buf->empty_sem, 0, BUF_SIZE)) handle_error_en(errno, "empty sem_init");
// 初始化 binary semaphore,初始值均为 1
if (sem_init(&buf->in_mtx, 0, 1)) handle_error_en(errno, "in_mtx sem_init");
if (sem_init(&buf->out_mtx, 0, 1)) handle_error_en(errno, "out_mtx sem_init");
}
void uninit_buffer(buffer_t *buf) {
if (sem_destroy(&buf->occupied_sem)) handle_error_en(errno, "occupied sem_destroy");
if (sem_destroy(&buf->empty_sem)) handle_error_en(errno, "empty sem_destroy");
if (sem_destroy(&buf->in_mtx)) handle_error_en(errno, "in_mtx sem_destroy");
if (sem_destroy(&buf->out_mtx)) handle_error_en(errno, "out_mtx sem_destroy");
}
2.3 屏障
屏障(Barrier),用于等待一系列目标任务的完成。也就是说,当只有部分线程完成任务时,它们会在屏障处阻塞等待,直到预期数量的所有线程都完成任务到达屏障。在最后一个线程到达屏障时,所有线程都将继续运行。
2.3.1 常用 API 函数
序号 | 函数 | 说明 |
---|---|---|
1 | int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned count); | 按照 attr 属性(可以为 NULL,即默认属性)初始化屏障 barrier ;count 为需要调用 pthread_barrier_wait() 进行同步等待的线程数量,必须大于零;不再使用 barrier 时,需要通过 pthread_barrier_destroy 进行销毁。 |
2 | int pthread_barrier_destroy(pthread_barrier_t *barrier); | 销毁屏障 barrier (不能正在被使用,如线程阻塞等待) |
3 | int pthread_barrier_wait(pthread_barrier_t *barrier); | 阻塞线程,直到指定数量的线程均调用了 pthread_barrier_wait() ,即完成一次线程同步。 |
关于返回值:
- 如果成功,函数均返回
0
; - 在内存等资源不足、参数错误等情况下,会返回相应错误码。
通过 man7.org/linux/man-pages 搜索 pthread_barrier
可以查看更多相关函数,以及更详细的说明。
2.3.2 使用示例
使用屏障协同数据处理。将数据分为多个批次,对于每个批次的数据:
- 由 0 号线程准备输入数据
input_data
,其中包含多个分片,每个分片会由不同的线程处理; - == 屏障同步点 1 ==
等待所有线程(主要是其它线程在等 0 号线程)到达此处后,开始对输入数据进行处理; - 每个线程对各自负责的
input_data
分片进行处理,生成output_result
分片; - == 屏障同步点 2 ==
等待所有线程到达此处后,形成完整的output_result
结果; - 再由 0 号线程对
output_result
进行整体处理(overall task)。
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
pthread_barrier_t barrier; // 全局屏障
// 模拟:生成输入数据、数据计算、计算结果后处理
void input_generate(int* input_data, int data_len) {
static int value = 1;
for (int i = 0; i < data_len; i++) {
input_data[i] = value++;
}
}
int data_process(int data) { return data * 2; }
void output_postprocess(int* output_result, int data_len) {
for (int i = 0; i < data_len; i++) {
output_result[i] += i ? output_result[i - 1] : output_result[data_len];
}
output_result[data_len] = output_result[data_len - 1];
}
struct {
int data_len;
int* input_data;
int* output_result;
} g_data;
void* thread_func(void* arg) {
int id = (int)(long)arg;
for (int i = 1; i <= 3; i++) {
if (id == 0) {
input_generate(g_data.input_data, g_data.data_len); // 0 号线程生成输入数据
}
printf("[Batch %d] Thread %d waiting for input data...\n", i, id);
pthread_barrier_wait(&barrier); // 等待输入数据准备好,所有线程开始处理
g_data.output_result[id] = data_process(g_data.input_data[id]); // 每个线程分别处理各自的数据
printf("[Batch %d] Thread %d finished data processing.\n", i, id);
pthread_barrier_wait(&barrier); // 等待所有线程完成数据处理
if (id == 0) {
printf("[Batch %d] Thread %d result postprocessing...\n\n", i, id);
output_postprocess(g_data.output_result, g_data.data_len); // 0 号线程对计算结果进行后处理
}
}
return NULL;
}
int main() {
const int concurrency = sysconf(_SC_NPROCESSORS_ONLN);
const int num_threads = concurrency > 0 ? concurrency : 2;
int input_data[num_threads];
int output_result[num_threads + 1]; // 多一个元素,临时存储上一次的结果
output_result[num_threads] = 0;
g_data.data_len = num_threads;
g_data.input_data = input_data;
g_data.output_result = output_result;
pthread_t threads[num_threads];
int ret; // 初始化屏障,需要等待 num_threads 个线程全部完成任务
if (ret = pthread_barrier_init(&barrier, NULL, num_threads)) handle_error_en(ret, "pthread_barrier_init");
for (int i = 0; i < num_threads; i++) {
if (ret = pthread_create(&threads[i], NULL, thread_func, (void*)(long)i)) handle_error_en(ret, "pthread_create");
}
for (int i = 0; i < num_threads; i++) {
pthread_join(threads[i], NULL);
}
if (ret = pthread_barrier_destroy(&barrier)) handle_error_en(ret, "pthread_barrier_destroy");
}
3 基于 C++ 的线程同步
3.1 条件变量(C++11)
3.1.1 类定义
类 std::condition_variable
,在 <condition_variable>
中定义。
序号 | 成员函数 | 说明 |
---|---|---|
1 | condition_variable(); | 构造函数。在资源不足、权限不足等情况下,会抛出异常。 |
2 | ~condition_variable(); | 析构函数。在析构前,应确保已向所有线程发送通知,使其可以解除阻塞。 |
3 | void notify_one() noexcept; | 唤醒“一个”等待的线程(如果存在)。详细参考 pthread_cond_signal() |
4 | void notify_all() noexcept; | 唤醒“所有”等待的线程(如果存在)。详细参考 pthread_cond_broadcast() |
5 | void wait(unique_lock<mutex>& lock); | 阻塞当前线程,直到被通知唤醒(或虚假唤醒)。详细参考 pthread_cond_wait() |
6 | template< class Predicate > void wait(unique_lock<mutex>& lock, Predicate pred); | pred 为可调用对象,返回 bool 类型,用于判断条件是否满足(过滤虚假唤醒)。等效代码: while (!pred()) wait(lock); |
7 | template< class Clock, class Duration > cv_status wait_until(unique_lock<mutex>& lock, const chrono::time_point<Clock, Duration>& abs_time); | 阻塞当前线程,直到被通知唤醒(或虚假唤醒),或者到达指定时间点。 |
8 | template< class Clock, class Duration, class Predicate > bool wait_until(unique_lock<mutex>& lock, const chrono::time_point<Clock, Duration>& abs_time, Predicate pred); | pred 用于判断条件是否满足。等效代码:while (!pred()) if (wait_until(lock, abs_time) == cv_status::timeout) return pred(); return true; |
9 | template< class Rep, class Period > cv_status wait_for(unique_lock<mutex>& lock, const chrono::duration<Rep, Period>& rel_time); | 阻塞当前线程,直到被通知唤醒(或虚假唤醒),或者超过指定时长。 等效代码: wait_until(lock, chrono::steady_clock::now() + rel_time); |
10 | template< class Rep, class Period, class Predicate > bool wait_for(unique_lock<mutex>& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred); | pred 用于判断条件是否满足。等效代码:wait_until(lock, chrono::steady_clock::now() + rel_time, std::move(pred)); |
11 | native_handle_type native_handle(); | 返回底层实现定义的条件变量句柄。在 Linux 系统中,返回 pthread_cond_t* 类型值。 |
延伸阅读:cppreference 说明和示例
3.1.2 使用示例
对于生产者和消费者问题,std::condition_variable
的使用示例:
// 编译指令:g++ -std=c++11 -pthread test.cpp
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <queue>
#include <thread>
#define PRODUCER_COUNT 2 // 多个生产者
#define MAX_ITEM_COUNT 10 // 处理元素的上限,生产/消费完毕后即退出
#define BUF_SIZE 2 // 缓冲区容量
struct CBuffer {
std::queue<char> item_queue; // 缓冲区
std::mutex mtx; // 互斥锁,保护缓冲区的访问
std::condition_variable more; // 向缓冲区放入(增加 more)元素的条件变量
std::condition_variable less; // 从缓冲区取出(减少 less)元素的条件变量
};
CBuffer buffer;
// 生产者将元素放入缓冲区
void insert_item(CBuffer *buf, char item, int id) {
std::unique_lock<std::mutex> lock(buf->mtx); // 获取缓冲区的互斥锁,以确保对缓冲区的独占访问
while (buf->item_queue.size() >= BUF_SIZE) { // 如果缓冲区已满,等待消费者取出元素
buf->less.wait(lock); // 原子地解锁并挂起等待,直到获得条件变量通知或发生虚假唤醒,重新加锁并返回函数
}
// 等价代码实现
// buf->less.wait(lock, [buf] { return buf->item_queue.size() < BUF_SIZE; });
buf->item_queue.push(item); // 将元素放入缓冲区
std::cout << "Producer[" << id << "] insert: " << item << std::endl;
lock.unlock(); // 如果不需要精确的事件调度,可以在通知前释放锁
buf->more.notify_one(); // 通知消费者有元素放入缓冲区
}
// 消费者从缓冲区中取出元素
char remove_item(CBuffer *buf) {
char item;
std::unique_lock<std::mutex> lock(buf->mtx); // 获取缓冲区的互斥锁,以确保对缓冲区的独占访问
// 在 condition_variable 实例上调用 wait(),传入锁对象和一个 lambda 函数;
// wait() 在内部调用传入的 lambda 函数,判断条件是否成立:
// - 若成立(lambda 函数返回 true),则 wait() 返回;
// - 否则(lambda 函数返回 false),wait() 解锁互斥,并令线程进入阻塞等待状态。
// 在收到条件变量通知后,线程将被唤醒,重新加锁,再次查验条件:
// - 若条件成立,则从 wait() 函数返回,此时互斥锁仍被锁住;
// - 若条件不成立,则线程解锁互斥,并继续等待。
buf->more.wait(lock, [buf] { return !buf->item_queue.empty(); });
// 等价代码实现
// while (buf->item_queue.empty()) {
// buf->more.wait(lock);
// }
item = buf->item_queue.front(); // 从缓冲区中取出元素
buf->item_queue.pop();
std::cout << "Consumer remove: " << item << std::endl;
lock.unlock(); // 如果不需要精确的事件调度,可以在通知前释放锁
buf->less.notify_one(); // 通知生产者有元素被取出
return item;
}
// 生产者线程函数
void producer_proc(int id) {
for (int i = id; i < MAX_ITEM_COUNT; i += PRODUCER_COUNT) { // 每个线程各自生产指定编号的数据
char item = '0' + (i % 10);
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟生产时间
insert_item(&buffer, item, id); // 将元素放入缓冲区
}
}
// 消费者线程函数
void consumer_proc() {
for (int item_idx = 0; item_idx < MAX_ITEM_COUNT; item_idx++) {
char item = remove_item(&buffer); // 从缓冲区中取出元素
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟消费时间
}
}
int main() {
std::thread consumer_thread(consumer_proc);
std::thread producer_threads[PRODUCER_COUNT];
for (int i = 0; i < PRODUCER_COUNT; i++) {
producer_threads[i] = std::thread(producer_proc, i);
}
consumer_thread.join();
for (int i = 0; i < PRODUCER_COUNT; i++) {
producer_threads[i].join();
}
return 0;
}
3.2 信号量(C++20)
3.2.1 类定义
复习回顾 线程池2-线程互斥 => 3.3 信号量。
3.2.2 使用示例
修改 条件变量示例 中的 CBuffer
定义和元素存取函数 insert_item()
、remove_item()
:
// 编译指令(需要 GCC 11 及以上):g++ -std=c++20 -pthread test.cpp
#include <chrono>
#include <iostream>
#include <queue>
#include <semaphore>
#include <thread>
#define PRODUCER_COUNT 2 // 多个生产者
#define MAX_ITEM_COUNT 10 // 处理元素的上限,生产/消费完毕后即退出
#define BUF_SIZE 2 // 缓冲区容量
struct CBuffer {
std::queue<char> item_queue; // 缓冲区
std::mutex mtx; // 互斥锁,保护缓冲区的访问
std::counting_semaphore<BUF_SIZE> occupied_sem{0}; // 缓存区中元素的个数(已被占用的槽的个数)
std::counting_semaphore<BUF_SIZE> empty_sem{BUF_SIZE}; // 缓存区中空槽的个数
};
CBuffer buffer;
// 生产者将元素放入缓冲区
void insert_item(CBuffer *buf, char item, int id) {
buf->empty_sem.acquire(); // 空槽个数信号量减 1,如果当前值不大于 0,则挂起等待消费者取出元素
buf->mtx.lock();
buf->item_queue.push(item); // 将元素放入缓冲区
std::cout << "Producer[" << id << "] insert: " << item << std::endl;
buf->mtx.unlock();
buf->occupied_sem.release(); // 元素个数信号量加 1
}
// 消费者从缓冲区中取出元素
char remove_item(CBuffer *buf) {
char item;
buf->occupied_sem.acquire(); // 元素个数信号量减 1,如果当前值不大于 0,则挂起等待生产者放入元素
buf->mtx.lock();
item = buf->item_queue.front(); // 从缓冲区中取出元素
buf->item_queue.pop();
std::cout << "Consumer remove: " << item << std::endl;
buf->mtx.unlock();
buf->empty_sem.release(); // 空槽个数信号量加 1
return item;
}
3.3 闩锁和屏障(C++20)
闩锁和屏障,用于等待一系列目标任务的完成。也就是说,当只有部分线程完成任务时,等待全部任务完成的线程会在屏障处阻塞,直到预期数量的所有线程都完成任务到达屏障。在最后一个线程完成任务到达屏障时,所有阻塞线程将继续运行。
闩锁(Latch)
闩锁包含一个向下计数器,计数器的值在创建闩锁时初始化。当线程运行到达闩锁 count_down()
/arrive_and_wait()
时,会递减计数(已完成当前线程负责的任务);如果在 wait()
/arrive_and_wait()
时,计数仍然大于零(还有其它线程负责的任务未完成),线程就会在闩锁处阻塞,直到计数递减到零(所有任务均已完成)。
- 计数器可以通过不同的线程进行递减,已可以通过一个线程进行多次递减;
- 无法增加或重置计数器,因此只能同步一次。
屏障(Barrier)
与闩锁不同,屏障是可重用的:当所有线程被解除阻塞后,屏障就可以再次使用。可以将屏障简单理解为包含“屏障计数”、“周期计数”和“完成函数”:
- 屏障计数:在每次开始同步时,计数器的初始计数(可以通过
drop
修改); - 周期计数:在一次同步过程中,计数器的实时计数(通过
arrive
动态更新); - 完成函数:在周期计数达到零之后,在解除所有线程阻塞(
wait
返回)之前,执行一个可为空的可调用对象。
一个屏障对象的生命周期可以包含一个或多个“同步周期”,一个“同步周期”内的操作包括:
- 在一个“同步周期”的开始,“周期计数”被重置为“屏障计数”;
- 在线程到达屏障
arrive()
/arrive_and_drop()
/arrive_and_wait()
时,“周期计数”会被递减; - 当“周期计数”达到零时,调用“完成函数”,随即解除所有
wait()
/arrive_and_wait()
阻塞的线程,并进入下一个“同步周期”。
3.3.1 类定义
3.3.1.1 类 std::latch
(在 <latch>
中定义)
序号 | 成员函数 | 说明 |
---|---|---|
1 | constexpr explicit latch(std::ptrdiff_t expected); latch(const latch&) = delete; | 1) 构造对象,并设置内部计数器的初始值为 expected (需 >= 0 并且 <= latch::max() );2) 既不能拷贝,也不能移动。 |
2 | ~latch(); | 销毁 *this 对象(不应并发调用) |
3 | operator= [deleted] | 不可以赋值。 |
4 | void count_down(std::ptrdiff_t n = 1); | 原子地将内部计数器减少 n ,不会阻塞调用者。n 应为非负数,并且不大于计数器当前的计数。 |
5 | bool try_wait() const noexcept; | 检测内部计数器是否已经达到 0 ,如果是则返回 true ,否则返回 false 。即使内部计数器已经达到 0 ,该函数也可能以极低的概率虚假地返回 false 。 |
6 | void wait() const; | 阻塞调用线程,直到内部计数器达到 0 ;如果计数器已经为 0 ,则立即返回。 |
7 | void arrive_and_wait(std::ptrdiff_t n = 1); | 原子地将内部计数器减少 n ,并阻塞调用线程(如果计数器没有达到 0 ),直到计数器达到 0 。等效代码: count_down(n); wait(); 。 |
3.3.1.2 类模板 std::barrier<CompletionFunction>
(在 <barrier>
中定义)
序号 | 成员函数 | 说明 |
---|---|---|
1 | constexpr explicit barrier(std::ptrdiff_t expected, CompletionFunction f = CompletionFunction()); barrier(const barrier&) = delete; | 1) 构造对象,设置内部“屏障计数”的初始值为 expected (需 >= 0 并且 <= barrier::max() ),使用 std::move(f) 初始化内部“完成函数”对象,并开始第一个“同步周期”,“周期计数”被初始为“屏障计数”;2) 既不能拷贝,也不能移动。 |
2 | ~barrier(); | 销毁 *this 对象(不应并发调用)。 |
3 | operator= [deleted] | 不可以赋值。 |
4 | arrival_token arrive(std::ptrdiff_t n = 1); | 原子地将内部“周期计数”减少 n ,返回与当前“同步周期”关联的 arrival_token 对象(不会阻塞调用者);n 应大于 0 ,并小于等于“周期计数”;如果在函数调用前,“周期计数”已经为 0 ,则行为未定义。 |
5 | void wait(arrival_token&& arrival) const; | 参数 arrival 由 arrive() 返回;如果 arrival 与 *this 当前的“同步周期”关联,则阻塞(如果“周期计数”没有达到 0 ),直到“周期计数”降为 0 ,并在“完成函数”执行后返回;如果 arrival 与 *this 前一个“同步周期”关联,则立即返回;如果 arrival 与 *this 更早的“同步周期”关联,或与 *this 以外的屏障对象的任何“同步周期”相关联,则行为未定义。在“周期计数”降为 0 后,会由参与同步的某个线程执行“完成函数”,之后所有阻塞线程被唤醒,wait() 函数返回。 |
6 | void arrive_and_wait(); | 原子地将内部“周期计数”减少 1 ,并阻塞(如果“周期计数”没有达到 0 ),直到当前“同步周期”的“周期计数”降为 0 ,并在“完成函数”执行后返回;等效代码: wait(arrive()); ,如果在函数调用前,“周期计数”已经为 0 ,则行为未定义。 |
7 | void arrive_and_drop(); | 原子地将内部“屏障计数”减少 1 (会导致后续“同步周期”的“周期计数”初始值也减少 1 ),将当前“同步周期”的“周期计数”减少 1 (不会阻塞调用者);如果在函数调用前,“周期计数”已经为 0 ,则行为未定义。 |
3.3.2 使用示例
3.3.2.1 闩锁 std::latch
示例
实现功能同 POXIS 屏障使用示例,由于闩锁只能同步一次,所以只处理一个批次的数据。
// 编译指令(需要 GCC 11 及以上):g++ -std=c++20 -pthread test.cpp
#include <iostream>
#include <latch>
#include <thread>
#include <vector>
// 模拟:生成输入数据、数据计算、计算结果后处理
void input_generate(int* input_data, int data_len) {
static int value = 1;
for (int i = 0; i < data_len; i++) {
input_data[i] = value++;
}
}
int data_process(int data) { return data * 2; }
void output_postprocess(int* output_result, int data_len) {
for (int i = 0; i < data_len; i++) {
output_result[i] += i ? output_result[i - 1] : output_result[data_len];
}
output_result[data_len] = output_result[data_len - 1];
}
int main() {
const int concurrency = std::thread::hardware_concurrency();
const int num_threads = concurrency > 0 ? concurrency : 2;
int input_data[num_threads] = {0};
int output_result[num_threads + 1] = {0};
std::latch latch(num_threads); // 1.创建闩锁对象,计数器初始值为 num_threads
input_generate(input_data, num_threads); // 2.主线程生成输入数据
auto thread_func = [&](int id) { // 线程函数
output_result[id] = data_process(input_data[id]); // 每个线程分别处理各自的数据
std::this_thread::sleep_for(std::chrono::seconds(id % 2)); // 模拟处理耗时
printf("Thread %d: finished data processing.\n", id);
latch.count_down(); // 完成数据处理,减少计数
printf("Thread %d: do more stuff.\n", id); // 可能会与主线程的后处理并发执行
std::this_thread::sleep_for(std::chrono::seconds(id % 2 + 1)); // 模拟其他操作
printf("Thread %d: done.\n", id);
};
std::vector<std::jthread> threads; // 在析构时,执行 join 操作
threads.reserve(num_threads);
for (int i = 0; i < num_threads; i++) {
threads.emplace_back(thread_func, i); // 3.创建线程,通过线程函数处理数据
}
printf("\nMain thread: waiting for all threads to count_down()...\n");
latch.wait(); // 4.主线程等待所有子线程完成数据处理
printf("\nMain thread: start result postprocessing...\n");
output_postprocess(output_result, num_threads); // 5.主线程对计算结果进行后处理
printf("\nMain thread: done.\n");
}
3.3.2.2 屏障 std::barrier
示例
实现功能同 POXIS 屏障使用示例,通过“完成函数”进行整体处理(overall task)。
// 编译指令(需要 GCC 11 及以上):g++ -std=c++20 -pthread test.cpp
#include <barrier>
#include <iostream>
#include <thread>
#include <vector>
// 模拟:生成输入数据、数据计算、计算结果后处理
void input_generate(int* input_data, int data_len) {
static int value = 1;
for (int i = 0; i < data_len; i++) {
input_data[i] = value++;
}
}
int data_process(int data) { return data * 2; }
void output_postprocess(int* output_result, int data_len) {
for (int i = 0; i < data_len; i++) {
output_result[i] += i ? output_result[i - 1] : output_result[data_len];
}
output_result[data_len] = output_result[data_len - 1];
}
int main() {
const int concurrency = std::thread::hardware_concurrency();
const int num_threads = concurrency > 0 ? concurrency : 2;
int input_data[num_threads] = {0};
int output_result[num_threads + 1] = {0}; // 多一个元素,临时存储上一次的结果
auto on_completion = [&]() noexcept { // 完成函数
printf("Thread (%x) result postprocessing...\n\n", std::this_thread::get_id());
output_postprocess(output_result, num_threads); // 对计算结果进行后处理
input_generate(input_data, num_threads); // 生成新的输入数据
};
std::barrier barrier(num_threads, on_completion); // 1.创建屏障对象,设置“完成函数” on_completion
input_generate(input_data, num_threads); // 2.由主线程生成初始输入数据
auto thread_func = [&](int id) { // 线程函数
for (int phase = 1; phase <= 3; phase++) {
output_result[id] = data_process(input_data[id]); // 每个线程分别处理各自的数据
printf("[Phase %d] Thread %d(%x) finished data processing.\n", phase, id, std::this_thread::get_id());
barrier.arrive_and_wait(); // 等待所有线程完成数据处理,然后由某个线程执行“完成函数”,随后进入下一个同步周期
}
};
std::vector<std::jthread> threads; // 在析构时,执行 join 操作
threads.reserve(num_threads);
for (int i = 0; i < num_threads; i++) {
threads.emplace_back(thread_func, i); // 3.创建线程,通过线程函数处理数据
}
}
3.4 future(C++11)
在线程同步场景中,有些任务只需要执行一次并同步结果,例如后台运行的计算任务完成并得出结果。在这种情况下,可以使用 std::future
进行同步。
也就是,如果一个线程需要等待另一个线程中"某个特定的一次性任务"完成(即等待异步任务的结果),那么它可以首先获取一个代表(未来的)该任务结果的 future
对象,然后选择进行如下某种处理:
- 直接等待
future
对象关联的任务完成; - 先执行其他任务,然后再等待
future
对象关联的任务完成; - 一边执行其他任务,一边检查
future
对象关联的任务是否已经完成。
一旦目标任务完成(即 future
对象变为就绪状态),就可以通过 future
对象获取任务结果。此后,future
对象将变为无效状态,不能再用于访问结果。
3.4.1 类定义
在标准库的 <future>
头中定义了几个与 std::future
相关的类型:
序号 | 类/函数 模板 | 说明 |
---|---|---|
1 | 类模板:future | 提供一种访问异步任务结果的机制,可以查询任务状态、等待任务完成,以及获取任务结果。 |
2 | 类模板:shared_future | 与 future 类似,但 future (只能移动)只允许一个线程访问某一异步任务的结果,而 shared_future (可以拷贝)允许多个线程共享访问“同一异步任务的结果”。 |
3 | 函数模板:async | 将某个可调用对象(如函数、lambda 表达式、bind 表达式或其他函数对象)作为异步任务执行,并返回与该任务结果关联的 future 对象。 |
4 | 类模板:packaged_task | 对可调用对象进行封装,使其可以作为异步任务执行,便于通过任务调度器进行管理。 可以通过成员函数 get_future() 获取与该任务结果关联的 future 对象。 |
5 | 类模板:promise | 通过 promise 对象可以显式设置异步任务的结果,而 async 和 packaged_task 只能通过可调用对象返回结果;promise 可以用于复杂的异步任务场景,而不局限于简单的函数调用。可以通过成员函数 get_future() 获取与任务结果关联的 future 对象。 |
std::future
使用场景示例:
- 获取
std::future
对象:- 可以通过
std::async
调用返回; - 可以通过
std::packaged_task
对象的成员函数get_future()
返回; - 可以通过
std::promise
对象的成员函数get_future()
返回;
- 可以通过
- 获取数据的线程在
std::future
对象上阻塞等待; - 提供数据的线程采用以下方法,使
std::future
对象变为就绪状态,唤醒阻塞线程:- 通过
std::async
传入的可调用对象返回结果; - 通过
std::packaged_task
封装的可调用对象返回结果; - 通过
std::promise
对象显式设置结果;
- 通过
- 获取数据的线程通过
std::future
对象获取结果。
3.4.1.1 类模板 future<T>
、future<T&>
、future<void>
序号 | 成员函数 | 说明 |
---|---|---|
1 | future() noexcept; future(future&& other) noexcept; future(const future& other) = delete; | 1) 默认构造,构造后,成员函数 valid() == false ;2) 移动构造,构造后 this->valid() 与移动前的 other.valid() 相同,移动后 other.valid() == false ;3) 不可以拷贝构造。 |
2 | ~future(); | 析构函数,释放共享状态(如果存在)。 共享状态(shared state),是在 future 对象和异步任务之间共享的状态数据,包括任务完成状态、任务结果,以及可能的异常等。在异步任务将结果或异常保存到共享状态后,共享状态(即 future 状态)变为就绪(ready,future_status::ready ),处于 wait() 等待中的 future 对象会收到就绪通知(所在线程被唤醒),此时可以获取异步任务的结果或异常。 |
3 | future& operator=(future&& other) noexcept; future& operator=(const future& other) = delete; | 1) 移动赋值,赋值后 this->valid() 与移动前的 other.valid() 相同,移动后 other.valid() == false ;2) 不可以拷贝赋值。 |
4 | shared_future<T> share() noexcept; | 将共享状态移动给新创建的 shared_future<T> 对象。等效代码:shared_future<T>(std::move(*this)) ,执行后 valid() == false 。 |
5 | bool valid() const noexcept; | 检查 this 是否拥有(引用,refers to)共享状态(shared state)。如果 this 通过 promise::get_future() 、packaged_task::get_future() 、async() 返回,则拥有(引用)共享状态,即 valid() 为 true ;或者, this 通过其它 valid() 为 true 的 future 对象移动得到,那么 valid() 也为 true ;在第一次调用 get() 后,valid() 会变为 false 。 |
6 | void wait() const; | 阻塞等待,直到异步任务的结果可用(共享状态就绪)。函数调用前,valid() 应该为 true ,函数调用后,valid() 仍然为 true 。 |
7 | template< class Clock, class Duration > future_status wait_until(const chrono::time_point<Clock,Duration>& timeout_time) const; | 阻塞等待,直到异步任务的结果可用(返回 ready ),或者到达指定时间点(返回 timeout );如果 this 由采用 launch::deferred 策略的 async() 返回,那么 wait_until() 将会立即返回 deferred 。 |
8 | template< class Rep, class Period > future_status wait_for(const chrono::duration<Rep,Period>& timeout_duration) const; | 阻塞等待,直到异步任务的结果可用(返回 ready ),或者超过指定时长(返回 timeout );如果 this 由采用 launch::deferred 策略的 async() 返回,那么 wait_for() 将会立即返回 deferred 。 |
9 | T get(); | 对于类模板 future<T> ,获取异步任务的结果,结果类型为 T ;在函数内部,通过调用 wait() 进行阻塞等待;在函数调用前, valid() 应该为 true ,在函数返回后,valid() 随即变为 false ;执行异步任务时,如果将异常保存在“共享状态”中,那么在调用 get() 时,将会抛出保存的异常。 |
10 | T& get(); | 对于特化模板 future<T&> ,获取异步任务的结果,结果类型为 T& (引用类型)。 |
11 | void get(); | 对于特化模板 future<void> ,不返回任何值,只用于等待异步任务完成。 |
3.4.1.2 类模板 shared_future<T>
、shared_future<T&>
、shared_future<void>
序号 | 成员函数 | 说明 |
---|---|---|
1 | shared_future() noexcept; shared_future(const shared_future& other); shared_future(shared_future&& other) noexcept; shared_future(future<T>&& other) noexcept; | 1) 默认构造,构造后,成员函数 valid() == false ;2) 拷贝构造,构造后 this 与 other 拥有(引用)相同的共享状态;3) 移动构造,将 other 的共享状态移动到 this ,构造后 this->valid() 与移动前的 other.valid() 相同,移动后 other.valid() == false ;4) 通过移动 future<T> 对象进行构造,效果同 3),从而获得与 promise 、packaged_task 、async() 关联的 shared_future 对象。 |
2 | ~shared_future(); | 析构函数,如果 *this 是最后一个引用共享状态的对象,则销毁共享状态,否则不做任何处理(减少引用计数?) |
3 | shared_future& operator=(const shared_future& other); shared_future& operator=(shared_future&& other) noexcept; | 1) 拷贝赋值,首先释放 this 的共享状态(如果存在),然后将 other 的共享状态赋值给 this ,即它们拥有(引用)相同的共享状态;2) 移动赋值,首先释放 this 的共享状态(如果存在),然后将 other 的共享状态移动到 this ,赋值后 this->valid() 与移动前的 other.valid() 相同,移动后 other.valid() == false 。 |
4 | bool valid() const noexcept; | 与 std::future 相同,检查 this 是否拥有(引用)共享状态。与 std::future 不同的是,在调用 get() 后,valid() 依然保持为 true 。 |
5 | void wait() const; | 与 std::future 相同,等待共享状态变为就绪;如果需要在多个线程中等待,那么每个线程应独立拥有 shared_future 对象的拷贝,以避免数据竞争;任务完成(共享状态就绪)后,所有拷贝都可以获取结果。 |
6 | template< class Clock, class Duration > future_status wait_until(const chrono::time_point<Clock,Duration>& timeout_time) const; | 与 std::future 相同 |
7 | template< class Rep, class Period > future_status wait_for(const chrono::duration<Rep,Period>& timeout_duration) const; | 与 std::future 相同 |
8 | const T& get() const; | 与 std::future 相同,类模板为 shared_future<T> ,结果类型为 const T& ;与 std::future 不同的是,在调用 get() 后,valid() 依然保持为 true ,共享状态在最后一个引用它的 shared_future 对象析构时销毁。 |
9 | T& get() const; | 与 std::future 相同,特化模板为 shared_future<T&> 。 |
10 | void get() const; | 与 std::future 相同,特化模板为 shared_future<void> 。 |
3.4.1.3 函数模板 async
/**
* @brief 根据 policy 策略,异步执行函数 f,并传入 args 参数;
* 返回 std::future 对象,用于访问 f 调用的结果(返回值或异常)。
* 在函数 f 执行完成后,共享状态变为就绪(ready),此时可以通过 future 对象获取函数结果。
*
* @param[in] policy 任务启动策略。
* 如果为 launch::async,会启动一个新线程来执行 f。
* The call to std::async "synchronizes with" the call to f.
* std::async 的调用与 f 的调用同步 (如果 操作A 与 操作B 同步,那么 A 的副作用对 B 可见)
* The associated thread completion "synchronizes-with"
* the successful return from the first function that is waiting on the shared state,
* or with the return of the last function that releases the shared state, whichever comes first.
* 关联线程的完成(即异步任务执行完毕),会与第一个“等待共享状态的函数”的成功返回同步;
* 如果没有调用等待函数,则与最后一个“释放共享状态的函数”(通常是 future 的析构函数)的返回同步。
* 如果为 launch::deferred,f 的调用会被推迟,直到 future 对象的 get() 或 wait() 方法被调用,
* 并且 f 会在调用 get() 或 wait() 的线程中执行(可以不同于调用 async() 的线程)。
* 调用 future 对象的 wait_for() 或 wait_until() 方法会立即返回 future_status::deferred。
* 如果为 launch::async | launch::deferred,由 std::async() 的实现自行选择运行方式。
*
* @param[in] f 可调用 (Callable) 对象,是需要异步执行的任务,
* 可以是函数、函数指针、lambda 表达式、bind 创建的对象、重载了函数调用运算符的类的对象等。
* 其类型 Function 需要可移动构造。
*
* @param[in] args 传递给 f 的参数;
* 对于 Args 中的所有类型,均需要可移动构造;
* 并且 f(std::forward<Args>(args)...) 应为有效的表达式。
*
* @return 返回 std::future<R> 对象,其中 R 是 f 的返回类型:decltype(f(std::forward<Args>(args)...))
* std::future<R> 对象拥有(引用,refers to)由 async() 创建的共享状态(shared state)。
*/
template< class Function, class... Args >
std::future<R> async( std::launch policy, Function&& f, Args&&... args );
3.4.1.4 类模板 packaged_task<R(ArgTypes...)>
序号 | 成员函数 | 说明 |
---|---|---|
1 | packaged_task() noexcept; template< class F > explicit packaged_task(F&& f); packaged_task(const packaged_task&) = delete; packaged_task(packaged_task&& rhs) noexcept; | 1) 默认构造,packaged_task 对象不关联任何可调用对象和共享状态;2) 模板构造,使用可调用对象 f 构造 packaged_task 对象(f 可匹配模板参数指定的函数签名 R(ArgTypes...) ),同时创建共享状态;3) 不可以拷贝构造; 4) 移动构造,将 rhs 的可调用对象和共享状态移动到 this 。 |
2 | ~packaged_task(); | 析构函数,放弃(abandon)共享状态,销毁保存的可调用对象。 放弃共享状态,即:如果共享状态已就绪,直接将其释放;如果共享状态未就绪,则存储一个类型为 std::future_error 的异常对象(错误码为 std::future_errc::broken_promise ),使共享状态变为就绪,然后将其释放。 |
3 | packaged_task& operator=(const packaged_task&) = delete; packaged_task& operator=(packaged_task&& rhs) noexcept; | 1) 不可以拷贝赋值; 2) 移动赋值,释放 this 的共享状态(如果有),销毁可调用对象;然后将 rhs 持有的共享状态和可调用对象移动到 this 。 |
4 | bool valid() const noexcept; | 检查 this 是否拥有共享状态和可调用对象。 |
5 | void swap(packaged_task& other) noexcept; | 交换 this 和 other 的共享状态和可调用对象。 |
6 | std::future<R> get_future(); | 返回 std::future<R> 对象,R 取决于模板参数指定的函数返回类型,该对象与 this 关联相同的共享状态(须存在);对于每个 packaged_task 对象,只能调用一次 get_future() 。 |
7 | void operator()(ArgTypes... args); | 调用 this 内部保存的可调用对象(只能调用一次),类似 INVOKE<R>(f, args...) ;ArgTypes 取决于模板参数指定的函数参数列表;将结果或异常保存到共享状态中,在函数执行后,共享状态变为就绪(ready),处于等待状态的线程(如果存在)会被唤醒。 |
8 | void make_ready_at_thread_exit(ArgTypes... args); | 类似 operator() ,不同的是,共享状态在“当前线程退出,并且销毁所有线程本地存储期对象”之后,才会变为就绪(ready)。 |
9 | void reset(); | 重置状态,放弃之前执行的结果,构造新的共享状态。 相当于 *this = packaged_task(std::move(f)) ,其中 f 是 this 内部保存的可调用对象。接下来需要重新调用 get_future() ,重新运行可调用对象。 |
3.4.1.5 类模板 promise<R>
、promise<R&>
、promise<void>
序号 | 成员函数 | 说明 |
---|---|---|
1 | promise(); promise(promise&& other) noexcept; promise(const promise& other) = delete; | 1) 默认构造,创建空(empty,没有任何值或异常)的共享状态; 2) 移动构造,将 other 的共享状态(可以没有)移动到 this ,构造后 other 没有(has no)共享状态;3) 不可以拷贝构造。 |
2 | ~promise(); | 析构函数,放弃(abandon)共享状态,即:如果共享状态已就绪,直接将其释放(has no);如果共享状态未就绪,则存储一个类型为 std::future_error 的异常对象(错误码为 std::future_errc::broken_promise ),并使共享状态变为就绪,然后将其释放(has no)。 |
3 | promise& operator=(promise&& other) noexcept; promise& operator=(const promise& rhs) = delete; | 1) 移动赋值,首先放弃 this 的共享状态,然后将 other 的共享状态移动到 this ,赋值后 other 没有(has no)共享状态,相当于执行 std::promise(std::move(other)).swap(*this) ;2) 不可以拷贝赋值。 |
4 | void swap(promise& other) noexcept; | 交换 other 和 this 的共享状态。 |
5 | std::future<R> get_future(); | 返回 std::future<R> 对象,该对象与 this 关联相同的共享状态(须存在);对于同一个共享状态,只能调用一次 get_future() 。 |
6 | void set_value(const R& value); void set_value(R&& value); | 对于类模板 promise<R> ,“原子地”将 value 存储到共享状态(须存在,并且为空)中,并使共享状态变为就绪(ready)。 |
7 | void set_value(R& value); | 对于特化模板 promise<R&> ,操作同上。 |
8 | void set_value(); | 对于特化模板 promise<void> ,不需要保存值,仅使共享状态变为就绪(ready)。 |
9 | void set_value_at_thread_exit(const R& value); void set_value_at_thread_exit(R&& value); void set_value_at_thread_exit(R& value); void set_value_at_thread_exit(); | 同 set_value ,但没有原子操作,不会立即让共享状态变为就绪(ready)。在“当前线程退出,并且销毁所有线程本地存储期对象”之后,共享状态才会变为就绪(ready)。 |
10 | void set_exception(std::exception_ptr p); | “原子地”将异常指针 p 存储到共享状态(须存在,并且为空)中,并使共享状态变为就绪(ready)。 |
11 | void set_exception_at_thread_exit(std::exception_ptr p); | 同 set_exception ,但没有原子操作,不会立即让共享状态变为就绪(ready)。在“当前线程退出,并且销毁所有线程本地存储期对象”之后,共享状态才会变为就绪(ready)。 |
3.4.2 使用示例
// 编译指令:g++ -std=c++11 -pthread test.cpp
#include <chrono>
#include <future>
#include <iostream>
#include <thread>
int foo(int x, int y) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
return x + y;
}
// 测试 async,通过 future::get() 阻塞等待结果
void test_async() {
std::future<int> f = std::async(std::launch::async, foo, 1, 2); // 异步执行 foo(1, 2)
std::cout << "async: " << f.get() << std::endl << std::endl; // 直接通过 f.get() 阻塞等待结果
}
// 测试 packaged_task,通过 future::wait_for() 判断任务是否完成
void test_packaged_task() {
std::packaged_task<int(int, int)> task(foo); // 模板参数指定函数签名,通过 packaged_task(F&& f) 构造对象
std::future<int> f = task.get_future();
std::thread t(std::move(task), 3, 4); // 在新线程中,异步执行 foo(3, 4)
std::cout << "packaged_task: launch" << std::endl;
while (f.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { // 判断任务是否完成
std::cout << "\t do other task" << std::endl; // 一边等待结果,一边处理其它任务
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟其他任务
}
std::cout << "packaged_task: " << f.get() << std::endl << std::endl; // 任务已完成,通过 f.get() 直接获取结果
t.join();
}
void bar(int x, int y, std::promise<int> prom) {
int result = foo(x, y); // 执行任务
prom.set_value(result); // 设置结果,并通知任务完成
}
void do_work(std::promise<void> prom) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
prom.set_value(); // 通知任务完成
}
// 测试 promise,通过 promise<int> 在线程间传递结果;通过 promise<void> 获取通知
void test_promise() {
// 使用 promise<int> 在异步任务完成时设置结果
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t(bar, 5, 6, std::move(p)); // 在新线程中,异步执行 bar(5, 6, promise<int>)
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟其他任务
std::cout << "promise<int>: " << f.get() << std::endl; // 处理其它任务后,再阻塞等待结果
t.join();
// 使用 promise<void> 在异步任务完成时发出通知
std::promise<void> p2;
std::future<void> f2 = p2.get_future();
std::thread t2(do_work, std::move(p2)); // 在新线程中,异步执行 do_work(promise<void>)
t2.detach();
f2.wait(); // promise<void>::set_value() 使共享状态 ready,唤醒等待线程,wait() 返回
std::cout << "promise<void>: done" << std::endl;
}
int main() {
test_async();
test_packaged_task();
test_promise();
}
4 同步技术对比
同步技术 | 条件变量 (Condition Variable) | 信号量 (Semaphore) | 屏障 (Barrier) | 闩锁 (Latch) | future<T> |
---|---|---|---|---|---|
应用场景 | 等待特定条件满足 | 协调对共享资源的访问 | 等待多个任务完成 | 同“屏障” | 获取一次性任务的结果 |
特点 | 1)必须与互斥锁配合使用; 2)如果没有线程等待,发出的信号会被忽略。 | 1)对信号量的访问是原子操作,无需加锁; 2)发出信号后,计数一定会增加。 | 可以多次同步 | 只能同步一次 | 访问异步任务结果,可以查询状态、等待完成、获取结果。 |
等待线程 伪代码示意 | mutex.lock(); while(!done){ cond.wait(mutex); } process(); mutex.unlock(); | sem.wait(); mutex.lock(); process(); mutex.unlock(); | 方式A:barrier.wait(); 方式B: barrier.arrive_and_wait(); | 方式A:latch.wait(); 方式B: latch.arrive_and_wait(); | 方式A:future.wait(); future.get_value(); 方式B: while(!future.ready()){ do_other_stuff(); } future.get_value(); |
通知线程 伪代码示意 | mutex.lock(); done=set_done(); cond.signal(); mutex.unlock(); | mutex.lock(); done=set_done(); mutex.unlock(); sem.post(); | 方式A:barrier.arrive(); 方式B: barrier.arrive_and_wait(); | 方式A:latch.count_down(); 方式B: latch.arrive_and_wait(); | 方式1:T async_func(){ return v; } 方式2: T packaged_task_func(){ return v; } 方式3: thread_func(){ promise<T>.set_value(v); } |
参考
- 朱文伟,李建英著.Linux C/C++ 服务器开发实践.清华大学出版社.2022.
- [英]安东尼·威廉姆斯著,吴天明译.C++并发编程实战(第2版).人民邮电出版社.2021
- man7:linux/man-pages
- cppreference: concurrency support library
- docs.oracle:Programming with Synchronization Objects
- wandbox:在线编译
宁静以致远,感谢 Mark 老师。