马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
一、先把上篇的结论接过来
上一篇我们从地址空间入手重新认识了线程,核心有四条,这里先复述一遍,它们是理解"线程控制"的前提:
- 物理空间不是连续的,是由 4KB 的内存块(页框)组成的。
- 页表映射通过虚拟地址来索引物理地址:一个虚拟地址共 32 位,前 10 位用来索引页目录中的元素(指向页表),中间 10 位用来索引页表中的对应元素(页框),后 12 位用来索引页框中的每一个字节(4KB = 4096 = 212)。
- 虚拟地址本质上是一种资源,可以分配。对一个进程的数据进行多份分配、每份走不同的执行流,就是多线程的本质。
- Linux 中的线程是通过进程模拟出来的——内核并没有单独设计一套独立的线程模块,线程就是一个共享
task_struct 的轻量级进程(LWP)。
由此还引申出两条常被忽略的事实:
- 进程从内核看,是承担分配系统资源的基本实体。
- Linux 中的执行流是线程,CPU 看到的执行流就是它。
二、线程控制的四件事:创建、终止、等待、分离
POSIX 线程库把对线程的操作收敛成四类动作,理解这四个词,整个 pthread API 就通了。
| 动作 | 函数 | 作用 |
| 创建 | pthread_create | 起一个新执行流 |
| 终止 | pthread_exit / pthread_cancel | 结束自己 / 取消别人 |
| 等待 | pthread_join | 阻塞等某个线程结束并回收其资源 |
| 分离 | pthread_detach | 声明"我不等它了",让它结束就自行回收 |
三、创建:pthread_create
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
- thread:出参,用来拿到新线程的 ID。
- attr:线程属性(栈大小、调度策略等),传 NULL 用默认。
- start_routine:线程的入口函数,返回
void*、参数为 void*。
- arg:传给入口函数的参数,不需要传就 NULL。
编译时要链 pthread:g++ main.cpp -o app -lpthread。
最常见的坑:传局部变量的地址
for (int i = 0; i < 5; ++i) {
pthread_create(&tid[i], NULL, worker, &i); // 错!
}
i 是循环变量,所有线程拿到的是同一个地址的当前值,跑起来会看到一堆相同的值甚至随机值。正确做法是给每次循环一个独立空间:
for (int i = 0; i < 5; ++i) {
int *p = new int(i);
pthread_create(&tid[i], NULL, worker, p);
// 或者用 malloc,或者定义 int arg[5] 分别传 &arg[i]
}
或者干脆用值传递——因为参数类型就是 void*,若只是传一个整数,很多实现允许直接强转((void*)i),但这是不可移植的写法,生产代码别用。
四、终止:三种方式
1. 入口函数 return
最简单:线程函数 return 就结束了,返回值可以通过 pthread_join 取到。这是推荐做法。
2. pthread_exit
void pthread_exit(void *retval);
在任意地方调用都能结束当前线程,retval 会被 join 拿到。注意它只结束线程,不结束进程。
3. pthread_cancel
int pthread_cancel(pthread_t thread);
注意:这个请求不保证成功。线程要在到达 cancellation point(取消点)时才真正响应——而"取消点"是一些会阻塞的系统调用(read、write、sleep、recv 等)。
- 一个纯计算的死循环线程可能永远收不到取消。
- 取消后线程会不会释放持有的锁?取决于取消类型(cancel state/type),默认情况下会执行清理处理,但这是很容易出错的细节。生产代码尽量别依赖 cancel。
任何线程调用 exit 或进程 exit,都是整个进程一起结束,其他线程一并没了——这是最容易搞混的一点。
五、等待:pthread_join
int pthread_join(pthread_t thread, void **retval);
调用它的线程会阻塞,直到 thread 指定的那个线程结束,然后回收其资源,并把 retval 取出来。
为什么一定要 join 或 detach
没有 join 也没有 detach 的线程,退出后其资源(尤其是线程栈)不会被回收,会一直泄漏,直到进程退出。这就是常说的"僵尸线程"现象。
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_join(tid, NULL); // 要么等它
// pthread_detach(tid); // 要么不管它
六、分离:pthread_detach
int pthread_detach(pthread_t thread);
分离的含义是:声明"我不关心这个线程什么时候结束,让它结束后自己回收资源"。调用之后就不能再 join 它了(join 一个已分离的线程会返回 ESRCH / EINVAL)。
典型用法:一个" fire-and-forget "的后台任务(比如定时上报心跳),主逻辑不关心它的结果,那就 create 之后立刻 detach,比配一个 join 数组干净得多。
attr 里也能预先设好
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, worker, NULL); // 一创建就是分离的
七、完整可运行示例
#include <pthread.h>
#include <iostream>
#include <vector>
void* worker(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 3; ++i) {
std::cout << "thread " << id << " step " << i << std::endl;
}
return NULL;
}
int main() {
pthread_t tid;
int arg = 7;
pthread_create(&tid, NULL, worker, &arg);
pthread_join(tid, NULL);
std::cout << "main done" << std::endl;
return 0;
}
编译:g++ main.cpp -o app -lpthread。这里 arg 是 main 栈上的变量、且在 join 返回前一直有效,所以传地址是安全的——这正好说明"能不能传地址"取决于它的生命周期是否比线程更长。
八、线程 ID:pthread_t 到底是什么
和 pid_t 不同,pthread_t 不保证是整数,glibc 上它实际是一个结构体指针,每个线程有自己的 pthread_t 值。
- 比较线程 ID 不能写
==,要用 pthread_equal()。
- 想把
pthread_t 打出来,用 %lu 或 %p,别用 %d。
- 真正的进程内线程数,可以用
/proc/<pid>/status 里的 Threads: 一行看到,排查"线程超多了"就查它。
九、和"进程"控制的对照
| 操作 | 进程 | 线程 |
| 创建 | fork() / vfork() | pthread_create() |
| 结束自己 | exit() | pthread_exit() |
| 取消别人 | kill() | pthread_cancel()(不保证生效) |
| 等待 | wait() / waitpid() | pthread_join() |
| 脱离关系 | 父进程无需等待(可忽略 SIGCHLD) | pthread_detach() |
小结
- Linux 的线程就是"共享资源的进程模拟",内核里没有独立的线程模块,执行流
task_struct 即线程。
- 线程控制四件套:
pthread_create 建、pthread_exit / pthread_cancel 终、pthread_join 等、pthread_detach 放。
- 关键纪律:每个线程要么被
join、要么被 detach,否则资源泄漏。
- 传参不要偷懒传循环变量的地址,要给每次调用独立空间。
pthread_cancel 只是请求,能不能停要看取消点;pthread_t 不是整数,比较要用 pthread_equal。
(本文由 qidao123.com企服评测-应用市场 编辑提示整理:示例基于 Linux glibc 的 POSIX 线程接口与 32 位 x86 页表模型,适用于 C/C++ 后端开发与多线程排障。) |