【Linux】从零开始熟悉多线程 --- 线程控制

[复制链接]
发表于 2026-10-3 16:21:17 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

一、先把上篇的结论接过来

上一篇我们从地址空间入手重新认识了线程,核心有四条,这里先复述一遍,它们是理解"线程控制"的前提:

  1. 物理空间不是连续的,是由 4KB 的内存块(页框)组成的。
  2. 页表映射通过虚拟地址来索引物理地址:一个虚拟地址共 32 位,前 10 位用来索引页目录中的元素(指向页表),中间 10 位用来索引页表中的对应元素(页框),后 12 位用来索引页框中的每一个字节(4KB = 4096 = 212)。
  3. 虚拟地址本质上是一种资源,可以分配。对一个进程的数据进行多份分配、每份走不同的执行流,就是多线程的本质。
  4. 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++ 后端开发与多线程排障。)

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表