搞懂了进程地址空间与线程的内存模型,我们顺着「合租公寓」的比喻,把线程控制的核心三件事一次性讲明白:怎么招新室友(线程创建)、室友怎么退租(线程终止)、退租后怎么收尾(线程等待)。搭配流程图、C/C++ 双语言可运行代码和思维导图,看完就能彻底掌握线程的完整生命周期。


一、线程创建:给公寓招一位新室友

线程创建,本质就是在现有进程(公寓)里新增一个独立执行流,相当于招一位新室友入住。操作系统会给新线程分配独有的私人资源,同时让它共享公寓的所有公共区域。

1. 创建时操作系统做了什么?

你只需要调用一个创建函数,背后操作系统会完成四件事:

  1. 在进程地址空间的栈区,给新线程分配一块独立的线程栈(私人卧室)
  2. 在内核里创建线程控制块(TCB),记录线程 ID、优先级、寄存器状态等信息
  3. 把你指定的「入口函数」和「参数」放到新线程的栈里,相当于给室友布置任务
  4. 把线程加入 CPU 调度队列,等待分配时间片开始执行

这也解释了为什么线程创建比进程快得多:进程要复制一整套地址空间(相当于盖新公寓),而线程只需要加个卧室和工牌,公共区域全复用。

2. C 语言 POSIX 实现

Linux 下标准线程库 pthread 的创建函数是 pthread_create,四个参数刚好对应招室友的完整流程:

c

运行

int pthread_create(
    pthread_t *tid,        // 输出参数:新线程的ID(给室友发工牌)
    const pthread_attr_t *attr,  // 线程属性:栈大小、优先级等,默认填NULL
    void *(*start_routine)(void *), // 线程入口函数(室友的本职工作)
    void *arg              // 传给入口函数的参数(给室友的工具/任务)
);

可运行代码示例:创建两个子线程分别执行任务

c

运行

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 线程1的任务:打扫卫生
void *clean_task(void *arg) {
    char *name = (char *)arg;
    printf("[%s] 开始打扫公寓...\n", name);
    sleep(2);
    printf("[%s] 打扫完成!\n", name);
    return NULL;
}

// 线程2的任务:做饭
void *cook_task(void *arg) {
    char *name = (char *)arg;
    printf("[%s] 开始做饭...\n", name);
    sleep(3);
    printf("[%s] 饭做好了!\n", name);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, clean_task, "室友A");
    pthread_create(&t2, NULL, cook_task, "室友B");
    
    printf("主线程:两个室友都开始干活了\n");
    sleep(5); 
    printf("主线程:公寓一天结束\n");
    return 0;
}

编译命令:gcc demo.c -o demo -lpthread

3. C++ 标准库实现

C++11 及以上提供了 std::thread,底层仍封装 pthread,语法更安全易用:

cpp

运行

#include <iostream>
#include <thread>
#include <unistd.h>

void clean_task(const std::string& name) {
    std::cout << "[" << name << "] 开始打扫公寓..." << std::endl;
    sleep(2);
    std::cout << "[" << name << "] 打扫完成!" << std::endl;
}

void cook_task(const std::string& name) {
    std::cout << "[" << name << "] 开始做饭..." << std::endl;
    sleep(3);
    std::cout << "[" << name << "] 饭做好了!" << std::endl;
}

int main() {
    std::thread t1(clean_task, "室友A");
    std::thread t2(cook_task, "室友B");

    std::cout << "主线程:两个室友都开始干活了..." << std::endl;
    t1.join();
    t2.join();

    std::cout << "主线程:公寓一天结束" << std::endl;
    return 0;
}

编译命令:g++ demo.cpp -o demo -std=c++11 -pthread 注意:std::thread 对象析构前必须调用 join()detach(),否则程序直接崩溃。

4. 经典踩坑:给线程传局部变量

很多初学者会写出这样的 bug:在循环里创建线程,把循环变量的地址传进去。

c

运行

// ❌ 错误写法
for(int i=0; i<3; i++){
    pthread_create(&tid, NULL, task, &i);
}

用公寓比喻很好理解:你在自己床头柜上写了个数字,转头就改,然后让室友去看这个数字。等室友真正去看的时候,数字早就变了,甚至你已经离开房间把变量销毁了。

✅ 正确做法:传值(强制转成 void*)或者动态分配内存传进去;C++ 中优先按值传参,引用必须用 std::ref 包装且保证变量生命周期长于线程。


二、线程终止:室友的三种「退租方式」

线程终止就是室友结束任务、离开公寓。终止方式有三种,体面程度不同,但都只会让线程自己离开,不会影响公寓和其他室友(除非搞破坏)。

方式 1:入口函数 return —— 体面主动退场

线程函数执行完 return 返回,相当于室友干完活,收拾好东西主动关门走人。这是最推荐、最安全的终止方式,返回值可以被其他线程获取,局部变量自动销毁。

方式 2:调用退出函数 —— 随时主动退房

在线程函数的任意位置调用退出函数,线程立刻终止,也可以携带返回值。

  • C 语言:pthread_exit(返回值)
  • C++:函数内任意位置 return 即可,不推荐暴力终止,优先用标志位协作退出

return 的区别:return 只是从当前函数返回,外层还有函数的话会继续执行;pthread_exit 直接终止整个线程,不管嵌套了多少层函数。

方式 3:被其他线程取消 —— 被动劝退

一个线程可以让另一个线程终止,相当于告诉室友 “你别干了,可以走了”。

  • C 语言:pthread_cancel(tid),注意取消不是立刻生效,线程需要走到「取消点」才会真正退出。
  • C++:标准库无强制取消接口,通用做法是用原子标志位协作式退出,避免资源泄漏。

❌ 绝对禁忌:在线程里调用 exit ()

exit() 是终止整个进程的! 不是终止线程。就像室友闹脾气,直接把整栋公寓炸了,所有室友全部强制退场。这是多线程编程里的经典低级错误。

C++ 协作式取消示例

cpp

运行

#include <iostream>
#include <thread>
#include <atomic>
#include <unistd.h>

std::atomic<bool> is_running = true;

void work_task() {
    while(is_running) {
        std::cout << "线程正在工作..." << std::endl;
        sleep(1);
    }
    std::cout << "线程收到退出信号,正常退场" << std::endl;
}

int main() {
    std::thread t(work_task);
    sleep(3);
    
    is_running = false;
    t.join();
    
    std::cout << "主线程:线程资源已回收" << std::endl;
    return 0;
}

三、线程等待:等室友交接完再收房

线程等待的作用是:阻塞等待指定线程结束,获取它的返回值,并回收线程的所有系统资源

用公寓比喻就是:室友说要走了,你站在卧室门口等他出来,接过他交还的钥匙和工作成果(返回值),然后进去打扫房间、回收床位(释放栈和 TCB 资源)。如果不等待回收,线程会变成「僵尸线程」,越积越多最终占满资源。

1. C 语言 POSIX 实现

函数 pthread_join,阻塞等待目标线程终止并回收资源:

c

运行

int pthread_join(
    pthread_t tid,   // 要等待的线程ID
    void **retval    // 二级指针,接收线程的返回值
);

基础等待示例

c

运行

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, clean_task, "室友A");
    pthread_create(&t2, NULL, cook_task, "室友B");
    
    printf("主线程:等待室友们干完活...\n");
    pthread_join(t1, NULL);
    printf("主线程:室友A已退场\n");
    
    pthread_join(t2, NULL);
    printf("主线程:室友B已退场\n");
    
    printf("主线程:所有人都走了,公寓关门\n");
    return 0;
}

获取返回值示例

c

运行

// 子线程:返回计算结果
void *calc_task(void *arg) {
    int num = *(int*)arg;
    int *result = malloc(sizeof(int));
    *result = num * 2;
    pthread_exit(result);
}

// 主线程:接收结果
int main() {
    pthread_t tid;
    int n = 10;
    pthread_create(&tid, NULL, calc_task, &n);
    
    void *ret;
    pthread_join(tid, &ret);
    int *res = (int*)ret;
    printf("计算结果:%d\n", *res);
    free(res);
    return 0;
}

2. C++ 标准库实现

C++ 不支持 join 直接拿返回值,推荐两种主流方案:

方案 A:引用传参输出结果(简单直观)

cpp

运行

#include <iostream>
#include <thread>

void calc_task(int num, int& result) {
    result = num * 2;
}

int main() {
    int result = 0;
    std::thread t(calc_task, 10, std::ref(result));
    t.join();

    std::cout << "计算结果:" << result << std::endl;
    return 0;
}
方案 B:std::promise + std::future(标准异步方案)

对应 C 版本 pthread_exit 携带返回值的能力,是 C++ 官方推荐的线程间安全传值方式。

cpp

运行

#include <iostream>
#include <thread>
#include <future>

void calc_task(int num, std::promise<int> prom) {
    int res = num * 2;
    prom.set_value(res);
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    std::thread t(calc_task, 10, std::move(prom));
    
    int result = fut.get();
    std::cout << "计算结果:" << result << std::endl;

    t.join();
    return 0;
}

四、线程分离:不用等的自主退房

如果有些后台线程(比如日志线程),我们不需要它的返回值,也不想专门等它收尾,可以设置为分离线程

比喻:室友入住时就说好 “我走的时候自己打扫干净房间,不用你送”。线程终止后,操作系统自动回收资源,不需要 join。分离后的线程不能再被 join,否则报错,适合不需要交互的后台常驻线程。

C 语言实现

c

运行

pthread_detach(tid); // 设置线程分离

C++ 实现

cpp

运行

#include <iostream>
#include <thread>
#include <unistd.h>

void daemon_task() {
    while(true) {
        std::cout << "[后台线程] 正在巡检公寓..." << std::endl;
        sleep(1);
    }
}

int main() {
    std::thread t(daemon_task);
    t.detach();

    std::cout << "主线程:前台业务运行中..." << std::endl;
    sleep(5);
    std::cout << "主线程:前台业务结束,程序退出" << std::endl;
    return 0;
}

五、线程生命周期全流程图解

我们把创建、运行、终止、等待回收整个流程串起来,形成完整的线程生命周期:

六、全文知识思维导图

最后用一张思维导图把线程控制的核心知识点全部串起来,方便复习巩固:

最后一句话总结

线程创建是给公寓添人,只加私人卧室、复用公共空间,成本极低;线程终止是人走了但资源没清;线程等待是收尾回收、交接成果。 所有线程控制机制,本质都是在「共享地址空间」这个大前提下,平衡并发效率与资源安全。

谢谢
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐