【组件-池式】线程池3-线程同步

声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。

摘要

总结整理 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();
  }
}

但是,上述方法并不理想:

  1. 不断检查标志,会浪费处理器的时间;
  2. 每次检查标志,都要通过互斥施加保护,会影响其它线程的运行;
  3. 在两次检查之间进行休眠,
    • 如果休眠期太短,线程会频繁检查,虚耗处理时间;
    • 如果休眠期太长,线程会因为“不能及时”获得任务完成的信息,而导致处理延迟。

更好的同步机制是:

对于一个线程 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 函数

序号函数说明
1int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);按照 attr 属性(可以为 NULL,即默认属性)初始化条件变量 cond
不再使用时,需要通过 pthread_cond_destroy 销毁。
2int pthread_cond_destroy(pthread_cond_t *cond);销毁条件变量 cond(不能正在被使用,如线程阻塞等待)
3pthread_cond_t cond = PTHREAD_COND_INITIALIZER;以静态方式初始化条件变量,使用 PTHREAD_COND_INITIALIZER(默认属性) 直接给条件变量赋值,不需要销毁。
4int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);mutex 解锁,同时(原子地)阻塞等待条件变量 cond
在调用函数前,应确保当前线程已对 mutex 加锁;
在函数返回时,当前线程已完成对 mutex 的再次加锁;
因为可能会发生虚假唤醒,所以在函数返回后,应该判断线程是否需要“继续阻塞,等待下一次唤醒”。
5int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);pthread_cond_wait(),除外:如果在时间 abstime 到达前,没有收到条件信号,函数将返回错误码 ETIMEDOUT,同时对 mutex 加锁。
6int pthread_cond_signal(pthread_cond_t *cond);将阻塞等待条件变量 cond 的“一个”线程解除阻塞;
如果没有线程在条件变量上阻塞,函数调用没有任何效果;
如果有多个线程在条件变量上阻塞,将根据调度策略决定线程解除阻塞的顺序;
需要确保与条件变量关联的互斥锁及时解锁,以使 pthread_cond_wait/timedwait() 能够及时加锁并返回。
7int 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 函数

序号函数说明
1int 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 进行销毁。
2int pthread_barrier_destroy(pthread_barrier_t *barrier);销毁屏障 barrier(不能正在被使用,如线程阻塞等待)
3int pthread_barrier_wait(pthread_barrier_t *barrier);阻塞线程,直到指定数量的线程均调用了 pthread_barrier_wait(),即完成一次线程同步。

关于返回值:

  • 如果成功,函数均返回 0
  • 在内存等资源不足、参数错误等情况下,会返回相应错误码。

通过 man7.org/linux/man-pages 搜索 pthread_barrier 可以查看更多相关函数,以及更详细的说明。

2.3.2 使用示例

使用屏障协同数据处理。将数据分为多个批次,对于每个批次的数据:

  1. 由 0 号线程准备输入数据 input_data,其中包含多个分片,每个分片会由不同的线程处理;
  2. == 屏障同步点 1 ==
    等待所有线程(主要是其它线程在等 0 号线程)到达此处后,开始对输入数据进行处理;
  3. 每个线程对各自负责的 input_data 分片进行处理,生成 output_result 分片;
  4. == 屏障同步点 2 ==
    等待所有线程到达此处后,形成完整的 output_result 结果;
  5. 再由 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> 中定义。

序号成员函数说明
1condition_variable();构造函数。在资源不足、权限不足等情况下,会抛出异常。
2~condition_variable();析构函数。在析构前,应确保已向所有线程发送通知,使其可以解除阻塞。
3void notify_one() noexcept;唤醒“一个”等待的线程(如果存在)。详细参考 pthread_cond_signal()
4void notify_all() noexcept;唤醒“所有”等待的线程(如果存在)。详细参考 pthread_cond_broadcast()
5void wait(unique_lock<mutex>& lock);阻塞当前线程,直到被通知唤醒(或虚假唤醒)。详细参考 pthread_cond_wait()
6template< class Predicate >
void wait(unique_lock<mutex>& lock, Predicate pred);
pred 为可调用对象,返回 bool 类型,用于判断条件是否满足(过滤虚假唤醒)。
等效代码:while (!pred()) wait(lock);
7template< class Clock, class Duration >
cv_status wait_until(unique_lock<mutex>& lock, const chrono::time_point<Clock, Duration>& abs_time);
阻塞当前线程,直到被通知唤醒(或虚假唤醒),或者到达指定时间点。
8template< 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;
9template< 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);
10template< 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));
11native_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 返回)之前,执行一个可为空的可调用对象。

一个屏障对象的生命周期可以包含一个或多个“同步周期”,一个“同步周期”内的操作包括:

  1. 在一个“同步周期”的开始,“周期计数”被重置为“屏障计数”;
  2. 在线程到达屏障 arrive()/arrive_and_drop()/arrive_and_wait() 时,“周期计数”会被递减;
  3. 当“周期计数”达到零时,调用“完成函数”,随即解除所有 wait()/arrive_and_wait() 阻塞的线程,并进入下一个“同步周期”。

3.3.1 类定义

3.3.1.1 std::latch(在 <latch> 中定义)

序号成员函数说明
1constexpr explicit latch(std::ptrdiff_t expected);
latch(const latch&) = delete;
1) 构造对象,并设置内部计数器的初始值为 expected(需 >= 0 并且 <= latch::max());
2) 既不能拷贝,也不能移动。
2~latch();销毁 *this 对象(不应并发调用)
3operator=[deleted]不可以赋值。
4void count_down(std::ptrdiff_t n = 1);原子地将内部计数器减少 n,不会阻塞调用者。
n 应为非负数,并且不大于计数器当前的计数。
5bool try_wait() const noexcept;检测内部计数器是否已经达到 0,如果是则返回 true,否则返回 false
即使内部计数器已经达到 0,该函数也可能以极低的概率虚假地返回 false
6void wait() const;阻塞调用线程,直到内部计数器达到 0
如果计数器已经为 0,则立即返回。
7void arrive_and_wait(std::ptrdiff_t n = 1);原子地将内部计数器减少 n,并阻塞调用线程(如果计数器没有达到 0),直到计数器达到 0
等效代码: count_down(n); wait();

3.3.1.2 类模板 std::barrier<CompletionFunction>(在 <barrier> 中定义)

序号成员函数说明
1constexpr 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 对象(不应并发调用)。
3operator=[deleted]不可以赋值。
4arrival_token arrive(std::ptrdiff_t n = 1);原子地将内部“周期计数”减少 n,返回与当前“同步周期”关联的 arrival_token 对象(不会阻塞调用者);
n 应大于 0,并小于等于“周期计数”;
如果在函数调用前,“周期计数”已经为 0,则行为未定义。
5void wait(arrival_token&& arrival) const;参数 arrivalarrive() 返回;
如果 arrival*this 当前的“同步周期”关联,则阻塞(如果“周期计数”没有达到 0),直到“周期计数”降为 0,并在“完成函数”执行后返回;
如果 arrival*this 前一个“同步周期”关联,则立即返回;
如果 arrival*this 更早的“同步周期”关联,或与 *this 以外的屏障对象的任何“同步周期”相关联,则行为未定义。
在“周期计数”降为 0后,会由参与同步的某个线程执行“完成函数”,之后所有阻塞线程被唤醒,wait() 函数返回。
6void arrive_and_wait();原子地将内部“周期计数”减少 1,并阻塞(如果“周期计数”没有达到 0),直到当前“同步周期”的“周期计数”降为 0,并在“完成函数”执行后返回;
等效代码:wait(arrive());,如果在函数调用前,“周期计数”已经为 0,则行为未定义。
7void 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_futurefuture 类似,但 future(只能移动)只允许一个线程访问某一异步任务的结果,而 shared_future(可以拷贝)允许多个线程共享访问“同一异步任务的结果”。
3函数模板:async将某个可调用对象(如函数、lambda 表达式、bind 表达式或其他函数对象)作为异步任务执行,并返回与该任务结果关联的 future 对象。
4类模板:packaged_task对可调用对象进行封装,使其可以作为异步任务执行,便于通过任务调度器进行管理。
可以通过成员函数 get_future() 获取与该任务结果关联的 future 对象。
5类模板:promise通过 promise 对象可以显式设置异步任务的结果,而 asyncpackaged_task 只能通过可调用对象返回结果;
promise 可以用于复杂的异步任务场景,而不局限于简单的函数调用。
可以通过成员函数 get_future() 获取与任务结果关联的 future 对象。

std::future 使用场景示例:

  1. 获取 std::future 对象:
    • 可以通过 std::async 调用返回;
    • 可以通过 std::packaged_task 对象的成员函数 get_future() 返回;
    • 可以通过 std::promise 对象的成员函数 get_future() 返回;
  2. 获取数据的线程在 std::future 对象上阻塞等待;
  3. 提供数据的线程采用以下方法,使 std::future 对象变为就绪状态,唤醒阻塞线程:
    • 通过 std::async 传入的可调用对象返回结果;
    • 通过 std::packaged_task 封装的可调用对象返回结果;
    • 通过 std::promise 对象显式设置结果;
  4. 获取数据的线程通过 std::future 对象获取结果。

3.4.1.1 类模板 future<T>future<T&>future<void>

序号成员函数说明
1future() 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 对象会收到就绪通知(所在线程被唤醒),此时可以获取异步任务的结果或异常。
3future& operator=(future&& other) noexcept;
future& operator=(const future& other) = delete;
1) 移动赋值,赋值后 this->valid() 与移动前的 other.valid() 相同,移动后 other.valid() == false
2) 不可以拷贝赋值。
4shared_future<T> share() noexcept;将共享状态移动给新创建的 shared_future<T> 对象。等效代码:shared_future<T>(std::move(*this)),执行后 valid() == false
5bool valid() const noexcept;检查 this 是否拥有(引用,refers to)共享状态(shared state)。
如果 this 通过 promise::get_future()packaged_task::get_future()async() 返回,则拥有(引用)共享状态,即 valid()true
或者,this 通过其它 valid()truefuture 对象移动得到,那么 valid() 也为 true
在第一次调用 get() 后,valid() 会变为 false
6void wait() const;阻塞等待,直到异步任务的结果可用(共享状态就绪)。函数调用前,valid() 应该为 true,函数调用后,valid() 仍然为 true
7template< 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
8template< 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
9T get();对于类模板 future<T>,获取异步任务的结果,结果类型为 T
在函数内部,通过调用 wait() 进行阻塞等待;
在函数调用前,valid() 应该为 true,在函数返回后,valid() 随即变为 false
执行异步任务时,如果将异常保存在“共享状态”中,那么在调用 get() 时,将会抛出保存的异常。
10T& get();对于特化模板 future<T&>,获取异步任务的结果,结果类型为 T&(引用类型)。
11void get();对于特化模板 future<void>,不返回任何值,只用于等待异步任务完成。

3.4.1.2 类模板 shared_future<T>shared_future<T&>shared_future<void>

序号成员函数说明
1shared_future() noexcept;
shared_future(const shared_future& other);
shared_future(shared_future&& other) noexcept;
shared_future(future<T>&& other) noexcept;
1) 默认构造,构造后,成员函数 valid() == false
2) 拷贝构造,构造后 thisother 拥有(引用)相同的共享状态;
3) 移动构造,将 other 的共享状态移动到 this,构造后 this->valid() 与移动前的 other.valid() 相同,移动后 other.valid() == false
4) 通过移动 future<T> 对象进行构造,效果同 3),从而获得与 promisepackaged_taskasync() 关联的 shared_future 对象。
2~shared_future();析构函数,如果 *this 是最后一个引用共享状态的对象,则销毁共享状态,否则不做任何处理(减少引用计数?)
3shared_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
4bool valid() const noexcept;std::future 相同,检查 this 是否拥有(引用)共享状态。
std::future 不同的是,在调用 get() 后,valid() 依然保持为 true
5void wait() const;std::future 相同,等待共享状态变为就绪;
如果需要在多个线程中等待,那么每个线程应独立拥有 shared_future 对象的拷贝,以避免数据竞争;任务完成(共享状态就绪)后,所有拷贝都可以获取结果。
6template< class Clock, class Duration >
future_status wait_until(const chrono::time_point<Clock,Duration>& timeout_time) const;
std::future 相同
7template< class Rep, class Period >
future_status wait_for(const chrono::duration<Rep,Period>& timeout_duration) const;
std::future 相同
8const T& get() const;std::future 相同,类模板为 shared_future<T>,结果类型为 const T&
std::future 不同的是,在调用 get() 后,valid() 依然保持为 true,共享状态在最后一个引用它的 shared_future 对象析构时销毁。
9T& get() const;std::future 相同,特化模板为 shared_future<T&>
10void 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...)>

序号成员函数说明
1packaged_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),使共享状态变为就绪,然后将其释放。
3packaged_task& operator=(const packaged_task&) = delete;
packaged_task& operator=(packaged_task&& rhs) noexcept;
1) 不可以拷贝赋值;
2) 移动赋值,释放 this 的共享状态(如果有),销毁可调用对象;然后将 rhs 持有的共享状态和可调用对象移动到 this
4bool valid() const noexcept;检查 this 是否拥有共享状态和可调用对象。
5void swap(packaged_task& other) noexcept;交换 thisother 的共享状态和可调用对象。
6std::future<R> get_future();返回 std::future<R> 对象,R 取决于模板参数指定的函数返回类型,该对象与 this 关联相同的共享状态(须存在);
对于每个 packaged_task 对象,只能调用一次 get_future()
7void operator()(ArgTypes... args);调用 this 内部保存的可调用对象(只能调用一次),类似 INVOKE<R>(f, args...)ArgTypes 取决于模板参数指定的函数参数列表;
将结果或异常保存到共享状态中,在函数执行后,共享状态变为就绪(ready),处于等待状态的线程(如果存在)会被唤醒。
8void make_ready_at_thread_exit(ArgTypes... args);类似 operator(),不同的是,共享状态在“当前线程退出,并且销毁所有线程本地存储期对象”之后,才会变为就绪(ready)。
9void reset();重置状态,放弃之前执行的结果,构造新的共享状态。
相当于 *this = packaged_task(std::move(f)),其中 fthis 内部保存的可调用对象。
接下来需要重新调用 get_future(),重新运行可调用对象。

3.4.1.5 类模板 promise<R>promise<R&>promise<void>

序号成员函数说明
1promise();
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)。
3promise& 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) 不可以拷贝赋值。
4void swap(promise& other) noexcept;交换 otherthis 的共享状态。
5std::future<R> get_future();返回 std::future<R> 对象,该对象与 this 关联相同的共享状态(须存在);
对于同一个共享状态,只能调用一次 get_future()
6void set_value(const R& value);
void set_value(R&& value);
对于类模板 promise<R>,“原子地”将 value 存储到共享状态(须存在,并且为空)中,并使共享状态变为就绪(ready)。
7void set_value(R& value);对于特化模板 promise<R&>,操作同上。
8void set_value();对于特化模板 promise<void>,不需要保存值,仅使共享状态变为就绪(ready)。
9void 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)。
10void set_exception(std::exception_ptr p);“原子地”将异常指针 p 存储到共享状态(须存在,并且为空)中,并使共享状态变为就绪(ready)。
11void 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);
}

参考

  1. 朱文伟,李建英著.Linux C/C++ 服务器开发实践.清华大学出版社.2022.
  2. [英]安东尼·威廉姆斯著,吴天明译.C++并发编程实战(第2版).人民邮电出版社.2021
  3. man7:linux/man-pages
  4. cppreference: concurrency support library
  5. docs.oracle:Programming with Synchronization Objects
  6. wandbox:在线编译

宁静以致远,感谢 Mark 老师。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值