线程基本介绍
主要作用 多线程的主要作用:引入多核能力,让程序能够利用多核处理器的并行计算能力,真正实现同时执行多个任务。线程是最小的调度单位。
多线程不依赖于硬件配置
- 即使在单处理器或单核系统上运行,多线程依然有以下好处:
- 改善响应时间;
- 提高吞吐量(某些线程阻塞时,其他线程仍可运行)。
- 多线程是一种软件层面的设计模型,与处理器数量无关。
线程私有数据
每个线程包含如下执行环境所需的信息:
信息项 | 描述 |
---|---|
线程 ID | 唯一标识线程 |
寄存器值 | 存储当前执行状态 |
栈 | 用于函数调用和局部变量存储 |
调度优先级和策略 | 影响线程被调度的方式 |
信号屏蔽字 | 控制线程对信号的响应 |
errno 变量 | 错误码(线程私有) |
线程私有数据 | 每个线程可拥有独立的数据副本 |
注意:除了上述线程私有数据外,所有线程共享进程的全局内存、堆、代码段、文件描述符等资源。
与进程比较
进程原语 | 线程原语 | 描述 |
---|---|---|
fork | pthread_create | 创建新的控制流 |
exit | pthread_exit | 从现有的控制流中退出 |
waitpid | pthread_join | 从控制流中得到退出状态 |
atexit | pthread_cancel_push | 注册在退出控制流时调用的函数 |
getpid | pthread_self | 获取控制流的 ID |
abort | pthread_cancel | 请求控制流的非正常退出 |
线程标识
就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程 ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
进程ID,它是用pid_t数据类型来表示的,是一个非负整数。线程ID是用pthread_t数据类型来表示的,实现的时候可以用一个结构来代表pthread_t数据类型,所以可移植的操作系统实现不能把它作为整数处理。因此必须使用一个函数来对两个线程ID进行比较。
函数 pthread_equal
int pthread_equal(pthread_t t1, pthread_t t2);
作用
pthread_equal
用于比较两个线程标识符(thread IDs)是否相等。由于线程 ID 应该被视为不透明类型,因此不能直接通过 ==
运算符来比较两个 pthread_t
类型的值,而应使用 pthread_equal
函数进行比较。
参数
pthread_t t1
:要比较的第一个线程 ID。pthread_t t2
:要比较的第二个线程 ID。
返回值
- 如果两个线程 ID 相等,则返回一个非零值。
- 如果两个线程 ID 不相等,则返回 0。
错误
此函数总是执行成功,不会失败。
函数 pthread_self
pthread_t pthread_self(void);
作用pthread_self()
用于获取调用线程的线程 ID。这个 ID 与创建该线程时在 pthread_create
调用中返回给 *thread
的值相同。
返回值
该函数总是成功,返回调用线程的 ID。
线程创建
pthread_create
可以使用pthread_create创建线程,默认情况下,新线程是可结合(joinable)的。可通过 pthread_attr_setdetachstate
将线程设置为分离(detached)状态。
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
作用pthread_create()
函数用于在调用进程中创建一个新的线程。新线程通过调用 start_routine
函数开始执行,参数 arg
会作为 start_routine
的唯一参数传入。
参数
pthread_t *restrict thread
:指向用于存储新线程 ID 的缓冲区指针。const pthread_attr_t *restrict attr
:指向用于设置新线程属性的结构体指针,若为NULL
则使用默认属性。void *(*start_routine)(void *)
:新线程开始执行的函数地址。void *restrict arg
:传递给start_routine
的参数。
返回值
- 成功时返回 0;
- 失败时返回错误码,此时
*thread
的内容是未定义的。
错误码
EAGAIN
:资源不足,无法创建新线程;或系统线程数达到限制(如RLIMIT_NPROC
、/proc/sys/kernel/threads-max
或/proc/sys/kernel/pid_max
)。EINVAL
:attr
中包含无效设置。EPERM
:没有权限设置attr
中指定的调度策略和参数。
线程终止
终止方式
新线程可以通过以下几种方式终止:
- 调用
pthread_exit
并指定退出状态,其他线程可以通过pthread_join
获取该状态。 - 从
start_routine
返回,等同于调用pthread_exit
并使用返回值作为退出状态。 - 被取消(参见
pthread_cancel
)。 - 进程中的任意线程调用了
exit
或主线程从main()
返回,这会导致进程中所有线程终止。
pthread_exit
[[noreturn]] void pthread_exit(void *retval);
作用pthread_exit()
函数用于终止调用它的线程,并可通过 retval
参数返回一个值。如果该线程是可连接的(joinable),则这个返回值可以被同一进程中的其他线程通过调用 pthread_join
获取。
在线程终止时,所有尚未弹出的清理处理程序(由 pthread_cleanup_push
建立)会按照与压入相反的顺序被弹出并执行。如果线程有线程特定数据(thread-specific data),在清理处理程序执行完毕后,相应的析构函数会被调用(调用顺序未指定)。
线程终止时,进程共享资源(如互斥锁、条件变量、信号量和文件描述符)不会被释放,也不会调用通过 atexit
注册的函数。 当一个进程中的最后一个线程终止时,整个进程会像调用了 exit
并传入退出状态码 0 一样终止,此时进程共享资源会被释放,且 atexit
注册的函数也会被调用。
参数
void *retval
:指向线程退出时返回的值。这个值可以被其他线程通过pthread_join()
获取。注意:该指针指向的内容不应位于调用线程的栈上,因为线程终止后其栈内容是未定义的。
返回值
- 此函数不会返回到调用者(标记为
[[noreturn]]
)。
错误
- 该函数总是成功,不会返回错误码。
注意事项
- 除主线程外,任何线程的起始函数中执行
return
操作,都会隐式调用pthread_exit()
,并将函数的返回值作为线程的退出状态。 - 为了让其他线程能够继续执行,主线程应调用
pthread_exit()
而不是exit
来终止自身。 retval
指向的数据不应位于调用线程的栈上,因为线程终止后栈的内容是未定义的。
栈的清理
与进程中的atexit
类似,线程可以安排退出时需要调用的函数,这样的函数称为线程清理处理程序(thread cleanup handler)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
作用pthread_cleanup_push
和 pthread_cleanup_pop
用于操作调用线程的线程取消清理处理程序栈。清理处理程序是一个在线程被取消(或在下面描述的各种其他情况下)时自动执行的函数;例如,它可以解锁互斥锁,以便进程中的其他线程可以使用该锁。
pthread_cleanup_push()
函数将 routine
推入清理处理程序栈的顶部。当 routine
稍后被调用时,它将接收 arg
作为参数。
pthread_cleanup_pop()
函数从清理处理程序栈顶移除routine,并在 execute
非零时选择性地执行它。
参数
void (*routine)(void *)
:指向清理处理程序函数的指针。该函数将在清理时被调用。void *arg
:传递给清理处理程序函数的参数。int execute
:非零值表示在弹出时执行清理处理程序;零值表示仅从栈中移除处理程序但不执行。
返回值
这些函数不返回值。
函数 pthread_cancel
#include <pthread.h>
int pthread_cancel(pthread_t thread);
作用pthread_cancel()
函数用于向指定的目标线程发送一个取消请求。目标线程是否以及何时响应这个取消请求,取决于该线程自身的两个属性:取消状态(cancelability state)和取消类型(cancelability type)。
取消状态:通过
pthread_setcancelstate
设置。- 启用 (PTHREAD_CANCEL_ENABLE):默认状态。接收到取消请求后,会根据取消类型来决定何时执行取消操作。
- 禁用 (PTHREAD_CANCEL_DISABLE):如果线程禁用了取消功能,那么取消请求会一直保持在队列中,直到该线程重新启用取消功能。
取消类型:在取消状态为“启用”时生效,通过
pthread_setcanceltype
设置。- 延迟 (PTHREAD_CANCEL_DEFERRED):默认类型。取消请求会被推迟,直到线程下次执行到一个取消点(cancelation point)函数时才被处理。常见的取消点函数包括
sleep()
,read()
,write()
,pthread_join()
等。 - 异步 (PTHREAD_CANCEL_ASYNCHRONOUS):线程可以在任何时候被立即取消,而无需等待到达一个取消点。
- 延迟 (PTHREAD_CANCEL_DEFERRED):默认类型。取消请求会被推迟,直到线程下次执行到一个取消点(cancelation point)函数时才被处理。常见的取消点函数包括
当线程最终响应取消请求时,它会执行一系列清理操作(如调用 pthread_cleanup_push
注册的清理函数)后终止。
参数
pthread_t thread
:要取消的目标线程的ID。
返回值
- 成功时返回 0。注意,这仅表示取消请求已成功发送或排队,并不意味着目标线程已经终止。
- 失败时返回一个非零的错误码。
ESRCH
:找不到具有指定thread
ID 的线程。
要确认一个线程是否真的因为取消而终止,需要使用 pthread_join()
来等待该线程,并检查其退出状态是否为 PTHREAD_CANCELED
。
线程属性
pthread_attr_t
程序示例
#include "pthread.h"
#include "stdio.h"
void *task_handle(void *arg) {
int *num = arg;
printf("%d\n", *num);
*num += 1;
return num;
}
int main() {
pthread_t thread;
int handle_arg = 2233;
pthread_create(&thread, NULL, task_handle, (void *)&handle_arg);
int *rtn;
pthread_join(thread, (void **)&rtn);
printf("线程执行完毕,返回%d\n", *rtn);
return 0;
}
➜ pthread ./thread_test
2233
线程执行完毕,返回2234
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void cleanup_handler(void *arg) {
printf("Cleanup handler called: %s\n", (char *)arg);
}
void *thread_func(void *arg) {
printf("Thread started. Thread ID: %lu\n", pthread_self());
// 注册清理函数
pthread_cleanup_push(cleanup_handler, "First cleanup");
pthread_cleanup_push(cleanup_handler, "Second cleanup");
// 让线程休眠一会儿,等待被取消
for (int i = 0; i < 5; ++i) {
printf("Thread running... %d\n", i);
sleep(1);
}
// 弹出清理函数(0 表示不执行,1 表示执行)
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
printf("Thread finished normally.\n");
pthread_exit((void *)0);
}
int main() {
pthread_t tid;
int ret;
// 创建线程
ret = pthread_create(&tid, NULL, thread_func, NULL);
if (ret != 0) {
perror("pthread_create");
exit(1);
}
// 主线程与新线程比较
if (pthread_equal(pthread_self(), tid)) {
printf("Main thread and new thread are the same.\n");
} else {
printf("Main thread and new thread are different.\n");
}
// 等待2秒后取消线程
sleep(2);
printf("Main thread cancels the new thread.\n");
pthread_cancel(tid);
void *res;
pthread_join(tid, &res);
if (res == PTHREAD_CANCELED) {
printf("Thread was canceled.\n");
} else {
printf("Thread exited normally.\n");
}
return 0;
}
输出结果:
➜ pthread ./thread_test
Main thread and new thread are different.
Thread started. Thread ID: 140113254692544
Thread running... 0
Thread running... 1
Main thread cancels the new thread.
Thread running... 2
Cleanup handler called: Second cleanup
Cleanup handler called: First cleanup
Thread was canceled.