声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
介绍在 Linux 环境中,使用 POSIX API 和 C++11 进行线程开发的基本操作,包括线程的创建、退出,以及属性设置等。
目录
1 基本概念
本章内容主要围绕线程的编程实现。
操作系统角度的线程描述,可以回顾 协程1-并发基础概念 => 3 线程。
1.1 线程函数
线程(入口/顶层)函数,就是线程进入运行态后要执行的函数,由程序自定义。
- 对于主线程(initial thread)而言,该函数是
main()
; - 对于其它线程,需要在创建线程的时候指明线程函数。
在线程的生命周期中,历经的状态包括:
- 创建,为线程分配栈空间、TCB 等资源;
- 刚刚被创建的线程,不一定马上执行,可能会处于就绪状态,等待处理器的调度。
- 执行,执行线程函数;
- 中断,暂停线程函数的执行;
- 恢复,从中断的地方开始,继续执行后面的代码;
- 结束,不再执行线程函数;
- 在线程函数结束后,有时不会立即释放线程所占用的系统资源。
- 销毁,释放栈空间、TCB 等资源。
- 对线程进行“分离”(detach)或“连接”(join)后,结束线程的资源会被释放;
- 线程也可能由于进程退出等原因而被强制销毁。
1.2 C++ 多线程开发方式
在 Linux C++ 开发环境中,通常有两种方式来开发多线程程序:
- 利用传统的 POSIX 多线程 API 函数;
- 利用较新的 C++11 线程类。
2 POSIX 线程 API
常用 API 函数:(头文件 pthread.h
,库 libpthread
)
序号 | 函数 | 说明 |
---|---|---|
1 | int pthread_create(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *); | 创建线程,需提供线程函数及其参数,可以设置线程属性 |
2 | int pthread_join(pthread_t, void **); | 阻塞等待一个线程的结束,并释放资源,可以获得线程返回值 |
3 | void pthread_exit(void *); | 在线程内部,终止自身执行 |
4 | pthread_t pthread_self(void); | 在线程内部,获取自身 ID |
5 | int pthread_cancel(pthread_t); | 取消一个线程的执行 |
6 | int 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 分离状态
线程的分离状态决定一个线程以什么样的方式终止,包括:
- 分离状态(
PTHREAD_CREATE_DETACHED
)- 这种线程能独立(分离)出去,可以自生自灭,线程运行结束时,其资源将立刻被系统回收。
- 可连接状态/非分离状态(
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 种:
SCHED_OTHER
(默认),轮转(分时)调度策略:- 系统为每个线程分配一段运行时间(时间片),轮流执行;
- 不支持优先级(最高和最低优先级都是
0
)。
SHCED_RR
,轮转调度策略,支持优先级抢占:- 系统为每个线程分配一个时间片,具有相同优先级的线程会被轮流调度;
- 具有更高优先级的任务到达时,会抢占 CPU,优先执行;
- 可设置的优先级范围是
1~99
。
SCHED_FIFO
,先来先服务调度策略,支持优先级抢占:- 按照线程创建的先后,CPU 执行完前面的线程后,再调度下一个线程;
- 线程一旦占用 CPU 则一直运行,直到有更高优先级的任务到达,或者自己放弃 CPU;
- 可设置的优先级范围是
1~99
。
Linux 的线程优先级是动态的,即使高优先级线程还没有完成,低优先级的线程还是会得到一定的时间片。
对于使用调度策略 SCHED_FIFO
或 SCHED_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, ¶m)) 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, ¶m)) 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, ¶m)) 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 下,线程结束的方法包括:
- 主动退出:
- 线程函数执行结束后,通过
return
返回(推荐使用); - 线程函数内部,调用
pthread_exit
函数退出;
- 线程函数执行结束后,通过
- 被动终止:
- 线程被同一进程中的其他线程通知结束或取消,例如用户终止耗时任务;
- 线程所属的进程结束了,例如进程调用了
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_push
和 pthread_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
用来关联某个线程,常用成员函数如下:
序号 | 成员函数 | 说明 |
---|---|---|
1 | thread() noexcept; | 默认构造函数。构造新的 thread 对象,但是,并没有任何与之相关联的(associated)线程。此调用后 get_id() 等于 std::thread::id() (即 joinable() 为 false ) |
2 | template<class Function, class... Args> explicit thread(Function&& f, Args&&... args); | 初始化构造函数。构造新的 thread 对象,并将它与线程关联。此调用后 get_id() 不等于 std::thread::id() (即 joinable() 为 true ) |
3 | thread(thread&& other) noexcept; | 移动构造函数。构造新的 thread 对象,并与参数 other 曾经关联的线程建立关联。此调用后 other 不再关联任何线程。 |
4 | ~thread(); | 析构 thread 对象。析构前 *this 应没有任何与之相关联的线程。如果 *this 拥有关联线程(即 joinable() 为 true ),则会调用 std::terminate() 。 |
5 | thread& operator=(thread&& other) noexcept; | 移动 thread 对象。移动前 *this 应没有任何与之相关联的线程。如果 *this 拥有关联线程(即 joinable() 为 true ),则会调用 std::terminate() 。 |
6 | bool joinable() const noexcept; | 检查 thread 对象是否可连接,即,是否有关联的线程,如果有则返回 true ;也就是,当 get_id() != thread::id() 时返回 true 。 |
7 | thread::id get_id() const noexcept; | 返回与 *this 关联的线程的 thread::id 。如果没有关联的线程,则返回默认构造的 thread::id() 。 |
8 | native_handle_type native_handle(); | 返回底层实现定义的线程句柄。对于 POSIX,对应的是 pthread_self() 返回的线程 ID。 |
9 | static unsigned int hardware_concurrency() noexcept; | 返回实现支持的并发线程数(为参考值)。 |
10 | void join(); | 阻塞当前调用线程,直至 *this 所关联的线程结束其执行,并释放其资源。对于同一 thread 对象(关联同一线程),不可以从多个线程调用其 join() 成员? |
11 | void detach(); | 从 thread 对象分离关联线程(不再关联),使其在后台独立运行(由 C++ 运行时库管理),一旦该线程退出,所有分配的资源都会被释放。 |
12 | void swap(std::thread& other) noexcept; | 交换两个 thread 对象的底层句柄(即关联的线程)。 |
注:
- 在下列操作后,
std::thread
对象会处于不关联任何线程的状态:- 被默认构造、被移动、已调用
detach()
、已调用join()
并返回后。
- 被默认构造、被移动、已调用
joinable()
取决于是否存在关联线程,而不是线程运行状态:- 即使线程函数已经结束运行,只要存在关联线程,
joinable()
即为true
,此时可以调用join()
或detach()
; - 即使线程函数正在运行,只要已经调用
detach()
,joinable()
即为false
(此时线程与任何thread
对象都无关联)。
- 即使线程函数已经结束运行,只要存在关联线程,
- 两个
std::thread
对象不能关联同一线程;std::thread
不可拷贝构造、不可拷贝赋值。
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::ref
或 std::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
命名空间,提供了管理当前线程的函数:
序号 | 函数 | 说明 |
---|---|---|
1 | std::thread::id get_id() noexcept; | 获取当前线程的 ID |
2 | template<class Rep, class Period> void sleep_for(const std::chrono::duration<Rep, Period>& sleep_duration); | 阻塞当前线程的执行,睡眠至少为 sleep_duration 时长。 |
3 | template<class Clock, class Duration> void sleep_until(const std::chrono::time_point<Clock,Duration>& sleep_time); | 阻塞当前线程的执行,直至到达 sleep_time 时间点。 |
4 | void 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)。
参考
- 朱文伟,李建英著.Linux C/C++ 服务器开发实践.清华大学出版社.2022.
- [英]安东尼·威廉姆斯著,吴天明译.C++并发编程实战(第2版).人民邮电出版社.2021
- man7: pthread.h
- cppreference: concurrency support library
宁静以致远,感谢 Mark 老师。