Linux 学习笔记(三十二):线程控制——创建、等待与分离

一、 线程的终止

线程有三种终止方式:

  1. return:函数返回。注意,主线程返回会导致整个进程退出。

  2. pthread_exit :谁调用谁退出,不影响其他线程。可以返回一个 void* 类型的指针给等待者。

  3. pthread_cancel:在另一个线程中调用,用于强制取消目标线程。

二、 线程的等待(pthread_join)

与进程的 wait 类似,线程退出后必须由其他线程调用 pthread_join 来回收资源,否则会造成内存泄漏。

  • pthread_join 会阻塞调用它的线程,直到目标线程结束。

  • 通过二级指针接收目标线程的返回值。

三、 线程的分离(pthread_detach)

如果主线程不需要关心子线程的结果,可以使用 pthread_detach 将线程分离。

  • 被分离的线程运行结束后,操作系统会自动回收它的资源。

  • 分离的线程不能被 pthread_join,强行 join 会报错。

四、 深度印证:线程 ID 的底层本质

(插入实战 4 的地址输出截图)

通过打印 pthread_self() 的返回值以及线程内部局部变量的地址,我们发现:

pthread_t 类型的线程 ID 本质上就是一个虚拟内存地址。它指向了进程共享区(mmap区域)中的一块内存(TCB),这块内存管理着该线程的私有栈和寄存器信息。

总结

熟练掌握线程的创建、退出、等待和分离,是多线程编程的第一步。牢记:用完线程一定要 join 或 detach,否则资源必将泄漏。

自我实战操作

咱们来写一段代码,把创建、等待、终止、分离全跑一遍。

bash 复制代码
cd ~/linux_study/thread_test
实战 1:线程的创建、带参数与返回值
cpp 复制代码
vim thread_return.c

(输入以下代码)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

// 线程执行的函数
void *thread_run(void *arg) {
    char *msg = (char *)arg;
    printf("子线程收到参数: %s\n", msg);
    
    // 动态分配返回值内存(不能在栈上分配,因为函数结束栈就销毁了)
    int *ret_val = (int *)malloc(sizeof(int));
    *ret_val = 666;
    
    printf("子线程准备退出...\n");
    pthread_exit((void *)ret_val); // 带返回值退出
}

int main() {
    pthread_t tid;
    char *msg = "Hello Thread!";
    
    // 1. 创建线程
    pthread_create(&tid, NULL, thread_run, (void *)msg);
    
    // 2. 等待线程结束,并获取返回值
    void *ret = NULL;
    pthread_join(tid, &ret);
    
    printf("主线程收到子线程的返回值: %d\n", *(int *)ret);
    free(ret); // 释放子线程 malloc 的内存
    
    return 0;
}

编译运行:

bash 复制代码
gcc thread_return.c -o thread_return -lpthread
./thread_return
实战 2:暗杀线程(pthread_cancel)
bash 复制代码
vim thread_cancel.c

(输入以下代码)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *thread_run(void *arg) {
    while(1) {
        printf("我是子线程,我在疯狂输出...\n");
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_run, NULL);
    
    sleep(3); // 让子线程先跑3秒
    
    printf("主线程受不了了,准备干掉子线程!\n");
    pthread_cancel(tid); // 发送取消请求
    
    void *ret;
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED) {
        printf("子线程被成功取消!\n");
    }
    
    return 0;
}

(编译运行 gcc thread_cancel.c -o thread_cancel -lpthread && ./thread_cancel)

👉 现象 :子线程输出 3 秒后,被主线程无情暗杀,pthread_join 返回的 ret 会变成宏 PTHREAD_CANCELED。

实战 3:体验线程分离(pthread_detach)
bash 复制代码
vim thread_detach.c

(输入以下代码)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void *thread_run(void *arg) {
    printf("子线程正在运行,马上要退出了...\n");
    sleep(1);
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_run, NULL);
    
    // 将子线程分离
    pthread_detach(tid);
    
    // 此时尝试 join 会直接报错(返回非0值)
    int ret = pthread_join(tid, NULL);
    if (ret != 0) {
        printf("主线程尝试 join 失败!错误码: %d (%s)\n", ret, strerror(ret));
    }
    
    printf("主线程自己睡觉去了,不管子线程了...\n");
    sleep(2);
    
    return 0;
}

👉 现象 :pthread_join 报错,说明分离后的线程无法被等待,系统会自动回收它。

实战 4:观察线程地址空间(印证文档)
bash 复制代码
vim thread_addr.c

(输入以下代码)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *thread_run(void *arg) {
    int local_var = 10; // 局部变量
    printf("子线程:pthread_self() = %lu, 局部变量地址 = %p\n", pthread_self(), &local_var);
    sleep(2);
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_run, NULL);
    
    sleep(1);
    int main_local = 20;
    printf("主线程:pthread_self() = %lu, 局部变量地址 = %p\n", pthread_self(), &main_local);
    
    pthread_join(tid, NULL);
    return 0;
}

(编译运行 gcc thread_addr.c -o thread_addr -lpthread && ./thread_addr)

👉 现象 :你会发现 pthread_self() 打印出来的值和子线程的局部变量地址(&local_var)非常接近。这证明了 pthread_t 本质上就是进程地址空间(共享区)里的一个地址。

相关推荐
瑞克-桑切斯1 小时前
PostgreSQL 14.4 源码部署 & 运维手册(CentOS7)
数据库·学习·postgresql
天天进步20151 小时前
Unity 2020.3 反复提示安全模式:一次 CS0009 与 DLL 文件占用的排查记录
unity·1024程序员节
计算机毕业编程指导师1 小时前
2027计算机毕设选题指南:网站AI化、大数据预测+算法(XGBoost/LightGBM)、深度学习三方向57题+选题方法论
大数据·python·计算机·毕业设计·课程设计·毕设选题·1024程序员节
xbzb1 小时前
服务器形态怎么选、BIOS 怎么进?从 1U 机架到固件启动项一次讲透
运维·服务器·bios·启动·固件·启动项
m0_639906941 小时前
JVM 核心基础|运行时数据区、类加载过程、双亲委派模型
1024程序员节
深圳市益普科技有限公司1 小时前
SMT MES与AOI、SPI的数据闭环:不良图怎么回写到工单
1024程序员节
Nuanyt1 小时前
LeetCode Hot 100 · 双指针 3 题 Java
java·笔记·算法·leetcode·双指针
夜雪一千1 小时前
Linux 权限、用户、用户组内容介绍
linux·运维·服务器
哥本哈士奇1 小时前
I love Power BI
1024程序员节