【组件-池式】线程池1-线程

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

摘要

介绍在 Linux 环境中,使用 POSIX API 和 C++11 进行线程开发的基本操作,包括线程的创建、退出,以及属性设置等。


1 基本概念

本章内容主要围绕线程的编程实现。
操作系统角度的线程描述,可以回顾 协程1-并发基础概念 => 3 线程

1.1 线程函数

线程(入口/顶层)函数,就是线程进入运行态后要执行的函数,由程序自定义。

  • 对于主线程(initial thread)而言,该函数是 main()
  • 对于其它线程,需要在创建线程的时候指明线程函数。

在线程的生命周期中,历经的状态包括:

  • 创建,为线程分配栈空间、TCB 等资源;
    • 刚刚被创建的线程,不一定马上执行,可能会处于就绪状态,等待处理器的调度。
  • 执行,执行线程函数;
  • 中断,暂停线程函数的执行;
  • 恢复,从中断的地方开始,继续执行后面的代码;
  • 结束,不再执行线程函数;
    • 在线程函数结束后,有时不会立即释放线程所占用的系统资源。
  • 销毁,释放栈空间、TCB 等资源。
    • 对线程进行“分离”(detach)或“连接”(join)后,结束线程的资源会被释放;
    • 线程也可能由于进程退出等原因而被强制销毁。

1.2 C++ 多线程开发方式

在 Linux C++ 开发环境中,通常有两种方式来开发多线程程序:

  1. 利用传统的 POSIX 多线程 API 函数;
  2. 利用较新的 C++11 线程类。

2 POSIX 线程 API

常用 API 函数:(头文件 pthread.h,库 libpthread

序号函数说明
1int pthread_create(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *);创建线程,需提供线程函数及其参数,可以设置线程属性
2int pthread_join(pthread_t, void **);阻塞等待一个线程的结束,并释放资源,可以获得线程返回值
3void pthread_exit(void *);在线程内部,终止自身执行
4pthread_t pthread_self(void);在线程内部,获取自身 ID
5int pthread_cancel(pthread_t);取消一个线程的执行
6int pthread_kill(pthread_t, int);向一个线程发送信号

通过 man7.org/linux 搜索 pthread 可以查看更多相关函数。

2.1 线程的创建

通过 pthread_create 创建子线程之后,父线程会继续执行 pthread_create 后面的代码;
为了避免“子线程还没有执行完,父线程就结束”,或者为了获取“子线程的工作结果”,可以通过 pthread_join 来等待子线程结束。

/**
 * @brief 创建线程。
 *        系统会为线程分配一个唯一的 ID 作为线程的标识。
 *
 * @param[out] pid 指向线程 ID 的指针,在创建成功后,返回线程 ID
 * @param[in] attr 指向线程属性结构的指针,如果为 NULL 则使用默认属性
 * @param[in] start_routine 指向线程函数的指针,可以是全局函数或类的静态函数
 *                          线程函数的参数类型、返回值类型均为 void*
 * @param[in] arg 指向线程函数参数的指针
 * @return 如果成功,返回 0,否则返回错误码
 */
int pthread_create(pthread_t *pid, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
/**
 * @brief 阻塞等待指定 ID 线程结束,并释放其资源。
 *        当前调用线程会挂起(即休眠,让出CPU),直到指定线程退出,
 *        指定线程退出后,调用线程会接收到系统的信号,从休眠中恢复。
 *
 * @param[in] pid 所等待线程的 ID
 * @param[out] value_ptr 用于接收线程函数返回值的指针,可以为 NULL
 * @return 如果成功,返回 0,否则返回错误码
 */
int pthread_join(pthread_t pid, void **value_ptr);

示例:创建线程,并等待其执行结束。

// 编译指令:g++ test.cpp -lpthread
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define handle_error_en(en, msg) \
  do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

typedef struct {
  int num;
  const char* str;
} data_t;

/**
 * @brief 线程函数
 * @param[in] arg 参数的实际类型及含义由程序自定义。
 *                在创建线程时,作为参数传入线程创建函数 pthread_create 中
 * @return 线程函数运行的结果,实际类型及含义由程序自定义
 */
void* thread_proc(void* arg) {
  data_t* data = (data_t*)arg;
  printf("[sub thread %lu] num=%d, str=%s\n", pthread_self(), data->num++, data->str);
  return data;  // 测试返回值
}

int main() {
  int ret;
  pthread_t pid;
  data_t data_in = {10, "hello"}, *pdata_out;
  if (ret = pthread_create(&pid, NULL, thread_proc, &data_in)) handle_error_en(ret, "pthread_create");
  if (ret = pthread_join(pid, (void**)&pdata_out)) handle_error_en(ret, "pthread_join");
  printf("[main thread %lu] num=%d, str=%s\n", pthread_self(), pdata_out->num, pdata_out->str);
}

2.2 线程的属性

POSIX 标准规定线程具有多个属性,包括:分离状态(Detached State)、调度策略和参数(Scheduling Policy and Parameters)、作用域(Scope)、栈尺寸(Stack Size)、栈地址(Stack Address)等。可以通过一组函数来获取和设置线程的属性值。

通过 pthread_create 创建线程时,如果属性参数为 NULL,那么创建的线程具有默认属性,即:可连接状态、栈大小为 8MB?,与父线程具有相同的调度策略。

#ifndef _GNU_SOURCE
#define _GNU_SOURCE  // 获取 pthread_getattr_np() 的声明
#endif
#include <errno.h>
#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>

#define handle_error_en(en, msg) \
  do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

void* thread_proc(void* arg) {
  int ret;
  pthread_attr_t gattr;  // 定义线程属性结构变量,获取当前线程属性值
  if (ret = pthread_getattr_np(pthread_self(), &gattr)) handle_error_en(ret, "pthread_getattr_np");

  int value;
  if (ret = pthread_attr_getdetachstate(&gattr, &value)) handle_error_en(ret, "getdetachstate");  // 获取线程分离状态
  printf("detach state = %s\n", value == PTHREAD_CREATE_DETACHED ? "DETACHED" : "JOINABLE");

  size_t size;
  if (ret = pthread_attr_getstacksize(&gattr, &size)) handle_error_en(ret, "getstacksize");  // 获取线程栈的大小
  printf("stack size = %luMB, min size = %dKB\n", size / 1024 / 1024, PTHREAD_STACK_MIN / 1024);

  if (ret = pthread_attr_getschedpolicy(&gattr, &value)) handle_error_en(ret, "getschedpolicy");  // 获取线程调度策略
  printf("sched policy = %s\n", (value == SCHED_OTHER)  ? "SCHED_OTHER"
                                : (value == SCHED_FIFO) ? "SCHED_FIFO"
                                : (value == SCHED_RR)   ? "SCHED_RR"
                                                        : "???");

  if (ret = pthread_attr_destroy(&gattr)) handle_error_en(ret, "pthread_attr_destroy");  // 释放属性结构资源
  return NULL;
}

int main() {
  int ret;
  pthread_t pid;
  if (ret = pthread_create(&pid, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");
  if (ret = pthread_join(pid, NULL)) handle_error_en(ret, "pthread_join");
}

2.2.1 分离状态

线程的分离状态决定一个线程以什么样的方式终止,包括:

  1. 分离状态(PTHREAD_CREATE_DETACHED
    • 这种线程能独立(分离)出去,可以自生自灭,线程运行结束时,其资源将立刻被系统回收。
  2. 可连接状态/非分离状态(PTHREAD_CREATE_JOINABLEB,默认)
    • 不会自动释放资源:当线程函数返回时,或调用 pthread_exit 结束时,都不会释放线程所占用的栈空间等资源;
    • 必须由其它线程回收资源:只有当其它线程对其执行 pthread_join 与其连接并返回后,才会释放资源。
    • 如果不调用 pthread_join,并且其它线程已先行退出,那么它将被 init 进程收养,init 进程将调用 wait 系列函数回收其资源。
#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)

void* thread_proc(void* arg) {
  sleep(1);
  printf("[sub thread %lu] will exit\n", pthread_self());
  return NULL;
}

int main() {
  int ret;
  pthread_t pid;

  // 将一个线程设置为分离状态有两种方式:
#if 1  // 1. 通过属性设置,直接创建分离线程
  pthread_attr_t sattr;
  if (ret = pthread_attr_init(&sattr)) handle_error_en(ret, "pthread_attr_init");  // 初始化一个线程属性结构体变量
  if (ret = pthread_attr_setdetachstate(&sattr, PTHREAD_CREATE_DETACHED)) handle_error_en(ret, "setdetachstate");
  if (ret = pthread_create(&pid, &sattr, thread_proc, NULL)) handle_error_en(ret, "pthread_create");
  if (ret = pthread_attr_destroy(&sattr)) handle_error_en(ret, "pthread_attr_destroy");  // 释放资源

#else  // 2. 把默认创建的可连接线程转换为分离线程
  if (ret = pthread_create(&pid, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");
  if (ret = pthread_detach(pid)) handle_error_en(ret, "pthread_detach");
#endif

#if 0
  sleep(2);  // 等待线程执行完成
  printf("[main thread %lu] will exit\n", pthread_self());
#else
  // 可以通过 thread_exit() 让主线程先退出(进程不退出),等到子线程退出了,进程才会退出
  printf("[main thread %lu] will exit\n", pthread_self());
  pthread_exit(NULL);
#endif
}

2.2.2 调度策略

线程调度:进程中有了多个线程后,就要管理这些线程如何占用 CPU;
调度策略:线程调度通常由操作系统来安排,不同操作系统的调度方法会有所不同。
Linux 的调度策略可以分为 3 种:

  1. SCHED_OTHER(默认),轮转(分时)调度策略:
    • 系统为每个线程分配一段运行时间(时间片),轮流执行;
    • 不支持优先级(最高和最低优先级都是 0)。
  2. SHCED_RR,轮转调度策略,支持优先级抢占:
    • 系统为每个线程分配一个时间片,具有相同优先级的线程会被轮流调度;
    • 具有更高优先级的任务到达时,会抢占 CPU,优先执行;
    • 可设置的优先级范围是 1~99
  3. SCHED_FIFO,先来先服务调度策略,支持优先级抢占:
    • 按照线程创建的先后,CPU 执行完前面的线程后,再调度下一个线程;
    • 线程一旦占用 CPU 则一直运行,直到有更高优先级的任务到达,或者自己放弃 CPU;
    • 可设置的优先级范围是 1~99

Linux 的线程优先级是动态的,即使高优先级线程还没有完成,低优先级的线程还是会得到一定的时间片。
对于使用调度策略 SCHED_FIFOSCHED_RR 的线程,如果在等待 mutex 互斥对象,那么在互斥对象解锁时,它们会按优先级顺序获得互斥对象。

#ifndef _GNU_SOURCE
#define _GNU_SOURCE  // 获取 pthread_getattr_np() 的声明
#endif
#include <errno.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define handle_error_en(en, msg) \
  do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

void* thread_proc(void* arg) {
  int ret;
  pthread_attr_t gattr;
  if (ret = pthread_getattr_np(pthread_self(), &gattr)) handle_error_en(ret, "pthread_getattr_np");

  int value;
  if (ret = pthread_attr_getschedpolicy(&gattr, &value)) handle_error_en(ret, "getschedpolicy");
  const char* policy = value == SCHED_OTHER ? "SCHED_OTHER" : (value == SCHED_FIFO ? "SCHED_FIFO" : "SCHED_RR");

  struct sched_param param;
  if (ret = pthread_attr_getschedparam(&gattr, &param)) handle_error_en(ret, "getschedparam");

  printf("[%lu] Policy = %s,\tpriority and range: %d [%d - %d]\n", pthread_self(), policy, param.sched_priority,
         sched_get_priority_min(value), sched_get_priority_max(value));

  if (ret = pthread_attr_destroy(&gattr)) handle_error_en(ret, "pthread_attr_destroy");
  if (ret = pthread_detach(pthread_self())) handle_error_en(ret, "pthread_detach");
  return NULL;
}

int main() {
  int ret;
  pthread_t pid;
  pthread_attr_t sattr;
  struct sched_param param = {11};  // 线程优先级

  // 默认参数
  if (ret = pthread_create(&pid, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");

  // 设置调度策略和优先级
  if (ret = pthread_attr_init(&sattr)) handle_error_en(ret, "pthread_attr_init");
  if (ret = pthread_attr_setinheritsched(&sattr, PTHREAD_EXPLICIT_SCHED)) handle_error_en(ret, "setinheritsched");
  if (ret = pthread_attr_setschedpolicy(&sattr, SCHED_RR)) handle_error_en(ret, "setschedpolicy");
  if (ret = pthread_attr_setschedparam(&sattr, &param)) handle_error_en(ret, "setschedparam");
  if (ret = pthread_create(&pid, &sattr, thread_proc, NULL)) handle_error_en(ret, "pthread_create");

  // 调整调度策略和优先级
  param.sched_priority = 22;
  if (ret = pthread_attr_setschedparam(&sattr, &param)) handle_error_en(ret, "setschedparam");
  if (ret = pthread_attr_setschedpolicy(&sattr, SCHED_FIFO)) handle_error_en(ret, "setschedpolicy");
  if (ret = pthread_create(&pid, &sattr, thread_proc, NULL)) handle_error_en(ret, "pthread_create");

  if (ret = pthread_attr_destroy(&sattr)) handle_error_en(ret, "pthread_attr_destroy");
  pthread_exit(NULL);
}

2.3 线程的退出

在 Linux 下,线程结束的方法包括:

  1. 主动退出:
    • 线程函数执行结束后,通过 return 返回(推荐使用);
    • 线程函数内部,调用 pthread_exit 函数退出;
  2. 被动终止:
    • 线程被同一进程中的其他线程通知结束或取消,例如用户终止耗时任务;
    • 线程所属的进程结束了,例如进程调用了 exit

2.3.1 线程主动结束

/**
 * @brief 在线程内部通过调用 pthread_exit() 函数终止执行。
 *        当进程中的最后一个线程终止后,进程也会终止,等同 exit(0)。
 *        在线程(入口/顶层)函数中执行 return,会隐式调用 pthread_exit(),并使用 return 的返回值作为其参数。
 *
 * @param[in] value_ptr 线程退出时的返回值。
 *                      如果线程是可连接的,该值可供在同一进程中调用 pthread_join() 的另一个线程使用。
 */
void pthread_exit(void *value_ptr);
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define handle_error_en(en, msg) \
  do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

void* thread_proc1(void* arg) {
  static int count = 100; // 静态变量,线程退出后仍然可访问
  pthread_exit(&count);
}

void* thread_proc2(void* arg) {
  static int count = 200;
  return &count;
}

int main() {
  int ret;
  pthread_t pid;
  int* pret;

  if (ret = pthread_create(&pid, NULL, thread_proc1, NULL)) handle_error_en(ret, "pthread_create");
  if (ret = pthread_join(pid, (void**)&pret)) handle_error_en(ret, "pthread_join");
  printf("thread_proc1 exitcode = %d\n", *pret);

  if (ret = pthread_create(&pid, NULL, thread_proc2, NULL)) handle_error_en(ret, "pthread_create");
  if (ret = pthread_join(pid, (void**)&pret)) handle_error_en(ret, "pthread_join");
  printf("thread_proc2 exitcode = %d\n", *pret);
}

2.3.2 pthread_kill 发送信号

在同一个进程中的其他线程,可以通过函数 pthread_kill 给要结束的线程发送信号,目标线程收到信号后再退出。

/**
 * @file <signal.h>
 * @brief 向指定 ID 的线程发送 signal 信号(异步)。
 *        接收信号的线程必须先用函数 sigaction/signal 注册该信号的处理函数,否则会影响整个进程;
 *        例如给一个线程发送了 SIGQUIT,但线程却没有实现 signal 处理函数,那么整个进程会退出。
 *
 * @param[in] pid 接收信号线程的 ID
 * @param[in] signal 发送的信号,通常是一个大于 0 的值,
 *                   如果等于 0,则用来探测线程是否存在(并不发送任何信号)
 * @return 如果成功,返回 0;
           否则返回错误码,其中 ESRCH 表示线程不存在;EINVAL 表示信号非法。
 */
int pthread_kill(pthread_t pid, int signal);
#include <errno.h>
#include <pthread.h>
#include <signal.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)

static void on_signal_term(int sig) {  // 信号处理函数
  printf("sub thread will exit\n");
  pthread_exit(NULL);
}

void* thread_proc(void* arg) {
  signal(SIGQUIT, on_signal_term);  // 注册信号处理函数

  for (int i = 10; i > 0; i--) {  // 模拟一个长时间计算任务
    printf("sub thread return left: %02ds\n", i);
    sleep(1);
  }
  return NULL;
}

int main() {
  int ret;
  pthread_t pid;
  if (ret = pthread_create(&pid, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");

  sleep(3);  // 让出 CPU,让子线程执行后,向子线程发送 SIGQUIT 信号,通知其结束
  if (ret = pthread_kill(pid, SIGQUIT)) handle_error_en(ret, "pthread_kill");
  if (ret = pthread_join(pid, NULL)) handle_error_en(ret, "pthread_join");

  printf("sub thread has completed, main thread will exit\n");
}

2.3.3 pthread_cancel 取消执行

在同一个进程中的其他线程,可以通过函数 pthread_cancel 来取消目标线程的执行。
取消某个线程的执行,也是发送取消请求,请求终止其运行。

/**
 * @brief 向指定 ID 的线程发送取消请求。
 *        发送取消请求成功,并不意味着目标线程立即停止运行,即系统并不会马上关闭被取消的线程;
 *        只有被取消的线程,下一次“在取消点,检测是否有未响应的取消信号时”,即:
 *        1)调用一些系统函数或 C 库函数(比如 printf、read/write、sleep 等)时,
 *        2)调用函数 pthread_testcancel(让内核去检测是否需要取消当前线程)时,
 *        才会真正结束线程。
 *        如果被取消线程成功停止运行,将自动返回常数 PTHREAD_CANCELED(‒1),通过 pthread_join 获得
 *
 * @param[in] pid 要被取消线程(目标线程)的线程 ID
 * @return 成功返回 0,否则返回错误码。
 */
int pthread_cancel(pthread_t pid);
#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)

void* thread_proc(void* arg) {
  for (int i = 10; i > 0; i--) {  // 模拟长时间计算任务
    printf("sub thread return left: %02ds\n", i);  // 内部取消点检测
    sleep(1);                                      // 内部取消点检测
    pthread_testcancel();                          // 主动让系统检测
  }
  return NULL;
}

int main() {
  int ret;
  pthread_t pid;
  if (ret = pthread_create(&pid, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");

  sleep(3);  // 让出 CPU,让子线程执行一会儿后,发送取消线程的请求
  if (ret = pthread_cancel(pid)) handle_error_en(ret, "pthread_cancel");

  long lret = 0;
  if (ret = pthread_join(pid, (void**)&lret)) handle_error_en(ret, "pthread_join");
  if (lret == (long)PTHREAD_CANCELED)
    printf("thread stopped with exit code: %ld\n", lret);
  else
    printf("some error occured (%ld)\n", lret);
}

2.4 线程资源释放时机

线程的“被动终止”存在一定的不可预见性,如何保证线程终止时能够顺利释放资源,特别是锁资源,是一个必须考虑的问题。
POSIX 线程库提供了函数 pthread_cleanup_pushpthread_cleanup_pop,让线程退出时可以做一些清理工作。

/**
 * @brief 把一个清理函数压入清理函数栈(先进后出)
 *
 * @param[in] routine 压栈的清理函数指针,清理函数会在以下情况下执行:
 *                 1) 调用 pthread_cleanup_pop 函数,且其参数为非 0 时,
 *                    弹出栈顶清理函数并执行。
 *                 2) 线程主动调用 pthread_exit 时(包括 return 和 pthread_kill),
 *                    栈中的所有清理函数被依次弹出并执行。
 *                 3) 线程被其他线程取消时(其他线程对该线程调用 pthread_cancel 函数),
 *                    栈中的所有清理函数被依次弹出并执行。
 * @param[in] arg 清理函数参数
 */
void pthread_cleanup_push(void (*routine)(void*), void* arg);
/**
 * @brief 弹出栈顶的清理函数,并根据参数来决定是否执行清理函数。
 *        必须和 pthread_cleanup_push 成对出现。
 *        在一对 push 和 pop 函数调用中间,使用 return、break、continue 和 goto 离开代码块的效果是未定义的。
 *
 * @param[in] execute 在弹出栈顶清理函数的同时,是否执行清理函数。
 *                    如果 execute 为 0,不执行;
 *                    如果 execute 非 0,则执行。
 */
void pthread_cleanup_pop(int execute);

示例:通过 pthread_cancel 取消线程,通过“清理函数”释放锁。

#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_mutex_t mutex;

void clean_proc(void* arg) {  // 清理函数
  int ret;
  if (ret = pthread_mutex_unlock(&mutex)) handle_error_en(ret, "pthread_mutex_unlock");
  printf("[%lu]: %02d clean_proc() unlock\n", pthread_self(), (int)(long)arg);
}

void* thread_proc(void* arg) {
  int ret;
  for (int i = 10; i > 0; i--) {
    pthread_cleanup_push(clean_proc, (void*)(long)i);  // 压栈一个清理函数 clean_proc
    if (ret = pthread_mutex_lock(&mutex)) handle_error_en(ret, "pthread_mutex_lock");  // 上锁
    printf("[%lu]: %02d lock\n", pthread_self(), i);

    sleep(1);  // 模拟一个临界资源操作,在收到 cancel 信号后,会退出

    if (ret = pthread_mutex_unlock(&mutex)) handle_error_en(ret, "pthread_mutex_unlock");  // 解锁
    printf("[%lu]: %02d unlock\n", pthread_self(), i);
    pthread_cleanup_pop(0);  // 弹出清理函数,但不执行(参数是 0)
  }
  return NULL;
}

int main() {
  int ret;
  long lret = 0;
  pthread_t pid1, pid2;
  if (ret = pthread_mutex_init(&mutex, NULL)) handle_error_en(ret, "pthread_mutex_init");

  if (ret = pthread_create(&pid1, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");
  if (ret = pthread_create(&pid2, NULL, thread_proc, NULL)) handle_error_en(ret, "pthread_create");

  sleep(5);  // 让出 CPU,让子线程执行

  // 取消 线程1 后,会在清理函数中解锁,线程2 能够继续执行
  if (ret = pthread_cancel(pid1)) handle_error_en(ret, "pthread_cancel");

  if (ret = pthread_join(pid1, (void**)&lret)) handle_error_en(ret, "pthread_join");
  printf("[%lu] stopped with exit code: %ld\n", pid1, lret);

  if (ret = pthread_join(pid2, (void**)&lret)) handle_error_en(ret, "pthread_join");
  printf("[%lu] stopped with exit code: %ld\n", pid2, lret);

  if (ret = pthread_mutex_destroy(&mutex)) handle_error_en(ret, "pthread_mutex_destroy");
}

3 C++11 线程类

在 C++11 标准中,引入了 5 个头文件来支持多线程编程,分别为:thread、mutex、atomic、condition_variable 和 future。
其中 thread 主要声明了 std::thread 类,另外包含了 std::this_thread 命名空间。

std::thread 用来关联某个线程,常用成员函数如下:

序号成员函数说明
1thread() noexcept;默认构造函数。构造新的 thread 对象,但是,并没有任何与之相关联的(associated)线程。
此调用后 get_id() 等于 std::thread::id()(即 joinable()false
2template<class Function, class... Args>
explicit thread(Function&& f, Args&&... args);
初始化构造函数。构造新的 thread 对象,并将它与线程关联。
此调用后 get_id() 不等于 std::thread::id()(即 joinable()true
3thread(thread&& other) noexcept;移动构造函数。构造新的 thread 对象,并与参数 other 曾经关联的线程建立关联。
此调用后 other 不再关联任何线程。
4~thread();析构 thread 对象。析构前 *this 应没有任何与之相关联的线程。
如果 *this 拥有关联线程(即 joinable()true),则会调用 std::terminate()
5thread& operator=(thread&& other) noexcept;移动 thread 对象。移动前 *this 应没有任何与之相关联的线程。
如果 *this 拥有关联线程(即 joinable()true),则会调用 std::terminate()
6bool joinable() const noexcept;检查 thread 对象是否可连接,即,是否有关联的线程,如果有则返回 true
也就是,当 get_id() != thread::id() 时返回 true
7thread::id get_id() const noexcept;返回与 *this 关联的线程的 thread::id
如果没有关联的线程,则返回默认构造的 thread::id()
8native_handle_type native_handle();返回底层实现定义的线程句柄。对于 POSIX,对应的是 pthread_self() 返回的线程 ID。
9static unsigned int hardware_concurrency() noexcept;返回实现支持的并发线程数(为参考值)。
10void join();阻塞当前调用线程,直至 *this 所关联的线程结束其执行,并释放其资源。
对于同一 thread 对象(关联同一线程),不可以从多个线程调用其 join() 成员?
11void detach();thread 对象分离关联线程(不再关联),使其在后台独立运行(由 C++ 运行时库管理),一旦该线程退出,所有分配的资源都会被释放。
12void swap(std::thread& other) noexcept;交换两个 thread 对象的底层句柄(即关联的线程)。

注:

  1. 在下列操作后,std::thread 对象会处于不关联任何线程的状态:
    • 被默认构造、被移动、已调用 detach()、已调用 join() 并返回后。
  2. joinable() 取决于是否存在关联线程,而不是线程运行状态:
    • 即使线程函数已经结束运行,只要存在关联线程,joinable() 即为 true,此时可以调用 join()detach()
    • 即使线程函数正在运行,只要已经调用 detach()joinable() 即为 false(此时线程与任何 thread 对象都无关联)。
  3. 两个 std::thread 对象不能关联同一线程;
    • std::thread 不可拷贝构造、不可拷贝赋值。
  4. std::terminate() 是 C++ 标准库函数,它会调用 std::terminate_handler(可以自定义),默认情况下, std::terminate_handler 会调用 std::abort 来异常终止程序。

在 Linux 中,类 std::thread 实现的底层依然是创建一个 pthread 线程并运行。因此,C++11 可以和 POSIX 结合使用(例如通过 pthread_setschedparam 设置调度策略),但不便移植。

3.1 线程的创建

在 C++11 中,通过类 std::thread 的构造函数来创建线程。
构造函数有三种形式:不带参数的默认构造函数、初始化构造函数、移动构造函数。

/**
 * @brief 初始化构造函数。构造新的 thread 对象,并将它与线程关联。
 *        线程立即开始执行(除非存在操作系统调度延迟);
 *        新创建的线程是可连接线程。
 *
 * @param[in] f 可调用 (Callable) 对象(线程函数),会被复制到新线程的存储空间中,并在那里被调用,由新线程执行。
 *              可以是函数、函数指针、lambda 表达式、bind 创建的对象、重载了函数调用运算符的类的对象等。
 *              线程函数(top-level function)的返回值将被忽略,
 *              可以通过 std::promise、std::async 或修改共享变量(可能需要同步),将其返回值或异常信息传递回调用方线程。
 *              如果线程函数因为抛出异常而终止,将会调用 std::terminate。
 *
 * @param[in] args 传递给线程函数的参数。
 *                 可以通过移动或按值复制传递;
 *                 如果传递引用参数,需要使用 std::ref 或 std::cref 进行包装(wrapped)。
 *                 另外,对于指针和引用,需要留意参数的生存期。
 */
template<class Function, class... Args>
explicit thread(Function&& f, Args&&... args);
/**
 * @brief 销毁 thread 对象。
 *        在销毁前,thread 对象应没有任何与之相关联的线程(即 joinable() 为 false)。
 *        在下列操作后,thread 对象无关联的线程,从而可以安全销毁:
 *            被默认构造、被移动、已调用 detach()、已调用 join() 并返回后
 *        在销毁时,如果 thread 对象拥有关联线程(即 joinable() 为 true),则会调用 std::terminate() 
 */
~thread();
/**
 * @brief 移动赋值运算符。
 *        在移动前,*this 应没有任何与之相关联的线程(即 joinable() 为 false),否则会调用 std::terminate()。
 *        在移动时,*this 会与 other 曾经关联的线程建立关联,即 this->get_id() 等于 other.get_id();
 *                 然后设置 other 为默认构造状态(不再关联任何线程)。
 *
 * @param[in] other 赋值给当前 thread 对象的另一个 thread 对象。
 *                  在此调用后,other.get_id() 等于 thread::id()
 *
 * @return *this
 */
thread& operator=(thread&& other) noexcept;
// 编译指令:g++ -std=c++11 test.cpp -lpthread
#include <chrono>    // std::chrono::milliseconds
#include <iostream>  // std::cout
#include <string>    // std::to_string
#include <thread>    // std::thread, std::this_thread::sleep_for

void f1(int n) {
  for (int i = 0; i < 5; ++i) {
    std::cout << "Thread f1 \t\t[" + std::to_string(i) + "] n=" + std::to_string(n) + '\n';
    ++n;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
  }
}

void f2(int& n) {
  for (int i = 0; i < 5; ++i) {
    std::cout << "Thread f2 \t\t[" + std::to_string(i) + "] n=" + std::to_string(n) + '\n';
    ++n;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
  }
}

class foo {
 public:
  void bar() {
    for (int i = 0; i < 5; ++i) {
      std::cout << "Thread foo::bar() \t[" + std::to_string(i) + "] n=" + std::to_string(n) + '\n';
      ++n;
      std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
  }
  int n = 0;
};

class baz {
 public:
  void operator()() {
    for (int i = 0; i < 5; ++i) {
      std::cout << "Thread baz() \t\t[" + std::to_string(i) + "] n=" + std::to_string(n) + '\n';
      ++n;
      std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
  }
  int n = 0;
};

int main() {
  int n = 0;
  foo f;
  baz b;
  std::thread t1;                   // t1 是 thread 对象,但它没有关联任何线程
  std::thread t2(f1, n + 1);        // 按值传递
  std::thread t3(f2, std::ref(n));  // 按引用传递
  std::thread t4(std::move(t3));    // t4 现在运行 f2(),t3 不再关联任何线程
  std::thread t5(&foo::bar, &f);    // t5 在对象 f 上运行 foo::bar()
  std::thread t6(b);                // t6 在对象 b 的副本上运行 baz::operator()
  t2.join();
  t4.join();
  t5.join();
  t6.join();
  std::cout << "Final value of n is " << n << '\n';               // 5
  std::cout << "Final value of f.n (foo::n) is " << f.n << '\n';  // 5
  std::cout << "Final value of b.n (baz::n) is " << b.n << '\n';  // 0
}

3.2 线程参数传递

在 C++11 中,向线程函数传递参数,只需向 std::thread 的构造函数添加相应的参数。

需要注意的是,因为线程具有内部存储空间,所以,

  • 首先,参数会被“复制”到新线程内部的存储空间,根据需要可能再进行隐式类型转换;
  • 然后,这些副本被当成临时变量,以右值形式传给线程函数。

3.2.1 复制传参

即便线程函数的相关参数按设想应该是引用,上述过程依然会发生:

// 构造 thread 对象 t,创建并关联一个线程,并在新线程上调用 f(3, "hello")
void f(int i, const std::string& s); // s 为 const 引用
std::thread t(f, 3, "hello");
// 0. 字符串的字面内容 "hello" 以指针 const char* 的形式传入构造函数;
// 1. 在新线程的存储空间,将 const char* 转换为 std::string 类型;
// 2. 以右值形式将 std::string 类型的临时变量传递给线程函数。

如果参数是指针,并且指向具有自动存储期的局部变量,那么这一过程可能会引发错误:

void f(int i, const std::string& s);
void oops(int num) {
  char buffer[1024];
  sprintf(buffer, "hello num %i", num);
  
  // 存在隐患:
  std::thread t(f, 3, buffer);  // buffer 在新线程内 被转换成 std::string 对象之前,
  t.detach();                   // oops() 函数可能已经退出,导致局部数组被销毁,从而引发未定义行为。

  // 解决方法:
  std::thread t2(f, 3, std::string(buffer));  // 在传入构造函数之前,就把 buffer 转化成 std::string 对象
  t2.detach();
}

3.2.2 引用传参

如果希望传递一个对象,又不复制它,就需要使用标准库函数 std::refstd::cref

#include <iostream>
#include <thread>

struct udata {
  int id_;
  int value_;
};

void init_udata(udata& data) {  // data 以非 const 引用传递
  data.id_ = 1;
  data.value_ = 100;
}

int main() {
  udata data;
  // 1. data 被复制到新线程存储空间,作为 move-only 类型临时变量(只能移动,不可复制)
  // 2. data 副本只能以右值形式传递,不能转换为线程函数参数所需的 左值引用 类型
  // std::thread t1(init_udata, data);  // 编译错误
  // t1.join();

  // 函数 std::ref 返回一个对象,包含给定的引用,此对象可以拷贝,可以赋值给左值引用
  std::thread t2(init_udata, std::ref(data));  // 正确,传递“指向变量 data 的引用”
  t2.join();
  std::cout << data.id_ << "," << data.value_ << "\n";
}

3.2.3 移动传参

如果希望移动参数,或者对于“只能移动、不能复制”的参数,可以传递右值实参。

例如 std::unique_ptr,它为动态分配的对象提供自动化的内存管理。在任何时刻,对于给定的对象,只能存在唯一一个 std::unique_ptr 实例指向它;通过移动构造(move constructor)函数和移动赋值运算符(move assignment operator),对象的归属权可以在不同的 std::unique_ptr 实例间转移;若该实例被销毁,所指对象也随之被销毁。

std::unique_ptr 类似,std::thread 类的实例也是能够移动(movable)却不能复制(not copyable),线程的归属权可以在不同的 thread 实例之间转移;对于任一特定的线程,任何时候都只有唯一的 thread 实例与之关联。

#include <iostream>
#include <thread>

struct udata {
  udata(int id) : id_(id) {  // 在主线程构造
    std::cout << "tid  " << std::this_thread::get_id() << ": constructor " << id_ << "\n";
  }
  ~udata() {  // 在子线程析构
    std::cout << "tid  " << std::this_thread::get_id() << ": destructor " << id_ << "\n";
  }
  int id_;
  int value_;
};

void thread_proc(std::unique_ptr<udata> pdata) {
  std::cout << "sub  " << std::this_thread::get_id() << ": proc " << pdata->id_ << "\n";
}

int main() {
  // udata 对象的归属权首先为 main(),然后进入新建线程的内部存储空间,最后转移给线程函数
  std::unique_ptr<udata> p(new udata(1));
  std::thread t1(thread_proc, std::move(p));  // 通过 std::move 将左值转为右值,传入线程内部,
                                              // 此后不应再访问 p 指向对象
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  std::cout << "main " << std::this_thread::get_id() << ": join 1\n";
  t1.join();

  std::thread t2(thread_proc, std::unique_ptr<udata>(new udata(2)));  // 直接构建右值(临时对象)
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  std::cout << "main " << std::this_thread::get_id() << ": join 2\n";
  t2.join();
}

3.3 线程标识符

线程的标识符 std::thread::id 可以用来唯一标识某个 thread 对象所关联的线程。

对于两个 std::thread::id 类型的对象:

  • 如果相等,那么它们表示相同的线程,或者它们都不表示任何线程(值均为 thread::id());
  • 如果不相等,那么它们表示不同的线程,或者其中一个表示某个线程,而另一个不表示任何线程。

在 Linux 中,thread::id 是对 pthread_t 的封装:

// 头文件 include/c++/9/thread
class thread {
 public:
  class id {
    native_handle_type _M_thread;  // 类型由实现定义,在 Linux 中对应 pthread_t

    friend bool operator==(thread::id __x, thread::id __y) noexcept;  // 支持比较
    friend bool operator<(thread::id __x, thread::id __y) noexcept;
    //...
  };

  thread::id get_id() const noexcept { return _M_id; }
  native_handle_type native_handle() { return _M_id._M_thread; }

 private:
  id _M_id;
  //...
};

inline bool operator==(thread::id __x, thread::id __y) noexcept {
   return __x._M_thread == __y._M_thread; 
}

示例:将 C++11 与 POSIX 结合,获取和设置线程调度策略:

#include <pthread.h>
#include <chrono>
#include <cstring>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex iomutex;
void f(int num) {
  std::this_thread::sleep_for(std::chrono::seconds(1));

  sched_param sch;
  int policy;
  pthread_getschedparam(pthread_self(), &policy, &sch);

  std::lock_guard<std::mutex> lk(iomutex);
  std::cout << "Thread " << num << " is executing at priority " << sch.sched_priority << '\n';
}

int main() {
  std::thread t1(f, 1), t2(f, 2);

  sched_param sch;
  int policy;
  pthread_getschedparam(t1.native_handle(), &policy, &sch);

  sch.sched_priority = 20;
  if (pthread_setschedparam(t1.native_handle(), SCHED_FIFO, &sch)) {
    std::cout << "Failed to setschedparam: " << std::strerror(errno) << '\n';
  }

  t1.join();
  t2.join();
}

3.4 当前线程 this_thread

C++11 通过 this_thread 命名空间,提供了管理当前线程的函数:

序号函数说明
1std::thread::id get_id() noexcept;获取当前线程的 ID
2template<class Rep, class Period>
void sleep_for(const std::chrono::duration<Rep, Period>& sleep_duration);
阻塞当前线程的执行,睡眠至少为 sleep_duration 时长。
3template<class Clock, class Duration>
void sleep_until(const std::chrono::time_point<Clock,Duration>& sleep_time);
阻塞当前线程的执行,直至到达 sleep_time 时间点。
4void yield() noexcept;让出当前线程的 CPU 时间片,为其他线程提供运行机会。
函数行为依赖于具体实现,特别是系统的调度策略和当前状态,如果当前没有其它同优先级的就绪线程,则 yield 可能无效。
#include <chrono>
#include <iostream>
#include <thread>

inline std::chrono::high_resolution_clock::time_point hr_now() { 
  return std::chrono::high_resolution_clock::now(); 
}

int main() {
  std::thread::id this_id = std::this_thread::get_id();
  std::cout << "thread id: " << this_id << " waiter...\n" << std::flush;

  std::chrono::milliseconds wait_duration(1000);
  std::chrono::duration<double, std::milli> elapsed;

  auto start = hr_now();
  std::this_thread::sleep_for(wait_duration);
  elapsed = hr_now() - start;
  std::cout << "sleep_for(1000ms) => " << elapsed.count() << "ms\n";

  start = hr_now();
  std::this_thread::sleep_until(std::chrono::system_clock::now() + wait_duration);
  elapsed = hr_now() - start;
  std::cout << "sleep_until(now() + 1000ms) => " << elapsed.count() << "ms\n";

  start = hr_now();
  auto end = start + wait_duration;
  do {
    std::this_thread::yield();
  } while (hr_now() < end);
  elapsed = hr_now() - start;
  std::cout << "yield() 1000ms => " << elapsed.count() << "ms\n";
}

3.5 线程的退出

在目标线程的处理流程自然结束之前,如果需要让另一线程向它发送停止信号,那么:

  • 可以针对每一种需要退出线程的情况,分别设计独立的工作方式;
  • 在《C++ 并发编程实战》的 9.2 中断线程 部分,采用 C++11 多线程编程中的 atomic、condition_variable、mutex 和 future 等特性,实现了一种通用的中断方式;
  • C++20 标准引入了 std::jithread,除了拥有 std::thread 的一般行为,还可以在特定情况下 被取消/停止(cancelled/stopped),以及在销毁时自动重新连接(rejoins)。

参考

  1. 朱文伟,李建英著.Linux C/C++ 服务器开发实践.清华大学出版社.2022.
  2. [英]安东尼·威廉姆斯著,吴天明译.C++并发编程实战(第2版).人民邮电出版社.2021
  3. man7: pthread.h
  4. cppreference: concurrency support library

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

  • 32
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值