【Linux系统】线程控制

发布于:2025-08-30 ⋅ 阅读:(15) ⋅ 点赞:(0)

1. POSIX线程库 (pthreads)

POSIX线程(通常称为pthreads)是IEEE制定的操作系统线程API标准。Linux系统通过glibc库实现了这个标准,提供了创建和管理线程的一系列函数。

核心特性

  • 命名约定:绝大多数函数都以 pthread_ 开头,这使得代码中线程相关的操作非常清晰易辨。

  • 头文件:使用 #include <pthread.h> 来包含所有数据类型和函数的声明。

  • 链接库:编译时必须添加 -lpthread 或 -pthread 链接选项来链接线程库。

    • -pthread 通常是更推荐的选择,因为它除了链接库之外,还可能定义必要的预处理宏,确保代码的可移植性。

为什么需要 -lpthread 选项?

这是一个非常重要的实践点。C语言的标准库(libc)默认不包含pthread函数的具体实现。这些实现存在于一个独立的共享库文件中,通常是 libpthread.so

  • -l 选项告诉链接器(ld)要去链接一个库。

  • pthread 是 libpthread.so 的简写(链接器会自动添加 lib 前缀和 .so 后缀)。

因此,-lpthread 的本质是:“链接器,请将我们的程序与名为 libpthread.so 的共享库链接起来,以便解析所有以 pthread_ 开头的函数。”


2. 线程创建

函数原型与核心机制

#include <pthread.h>
int pthread_create(
    pthread_t *thread,               // 线程标识符(输出参数)
    const pthread_attr_t *attr,       // 线程属性(可为NULL)
    void *(*start_routine)(void *),  // 线程入口函数指针
    void *arg                         // 入口函数的参数
);

1. pthread_t *thread - 线程标识符

  • 用途:这是一个输出参数,函数成功返回后,会在此处填充新创建线程的标识符。

  • 本质pthread_t 是一个不透明的数据类型,通常是一个整数或结构体指针,具体实现取决于系统(Linux中为unsigned long,macOS中为结构体)。

  • 重要提示:不要假设 pthread_t 是整数类型,如果需要比较线程ID,应使用 pthread_equal() 函数。获取当前线程ID使用 pthread_self()

2. const pthread_attr_t *attr - 线程属性

  • 用途:指定新线程的属性。如果为 NULL,则使用默认属性。

  • 可配置属性包括:

    • 分离状态(detached state)

    • 调度策略和参数(scheduling policy and parameters)

    • 栈大小(stack size)

    • 栈地址(stack address)

    • 守卫区大小(guard size)

    • 线程的竞争范围(contention scope)

3. void *(*start_routine)(void*) - 线程函数

  • 形式:线程函数必须符合特定的签名 - 接受一个 void* 参数并返回一个 void* 值。

  • 执行流程:新线程从 start_routine 函数的开始处执行,直到:

    1. 函数返回(线程隐式终止)

    2. 调用 pthread_exit()(线程显式终止)

    3. 被其他线程取消(pthread_cancel()

  • 返回值:线程函数的返回值可以通过 pthread_join() 获取。

4. void *arg - 线程参数

  • 用途:传递给线程函数的参数。

  • 灵活性:由于是 void* 类型,可以传递任何数据类型的地址。

  • 注意事项

    • 确保参数在线程使用期间保持有效

    • 如果传递栈上变量的地址,要确保原函数不会在线程使用前返回

    • 通常使用动态分配的内存或全局变量传递数据

返回值与错误处理

  • 成功:返回 0

  • 失败:返回错误码(非零值),不设置 errno

  • 常见错误码

    • EAGAIN:系统资源不足,无法创建线程,或已超过线程数量限制

    • EINVALattr 参数无效

    • EPERM:没有权限设置指定的调度策略或参数

示例:

#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>

void *routine(void *arg)
{
    std::string name = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "我是一个新线程: " << name << ", pid: " << getpid() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void *)"thread-1");

    while (true)
    {
        std::cout << "main主线程, pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果:

ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadControl$ ./test
main主线程, pid: 221797
我是一个新线程: thread-1, pid: 221797
main主线程, pid: 我是一个新线程: thread-1221797, pid: 
221797
main主线程, pid: 221797
我是一个新线程: thread-1, pid: 221797
main主线程, pid: 221797
我是一个新线程: thread-1, pid: 221797
main主线程, pid: 221797
我是一个新线程: thread-1, pid: 221797
main主线程, pid: 221797
main主线程, pid: 221797
main主线程, pid: 221797
main主线程, pid: 221797
main主线程, pid: 221797
main主线程, pid: 221797

注意:创建新线程后,两个线程同时向显示屏输出,会造成数据竞争,导致打印的输出信息混在了一起

通过 ps -aL 指令可以查看,-L 选项:打印线程信息

ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadControl$ while :; do ps -aL | head -1 && ps -aL | grep test ; sleep 1 ; done
    PID     LWP TTY          TIME CMD
    PID     LWP TTY          TIME CMD
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
 221797  221798 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
 221797  221798 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
 221797  221798 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
 221797  221798 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
 221797  221798 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
 221797  221797 pts/4    00:00:00 test
    PID     LWP TTY          TIME CMD
    PID     LWP TTY          TIME CMD
    PID     LWP TTY          TIME CMD

PID (进程ID): 两个线程都有相同的PID (221797)。这证明了它们属于同一个进程。

LWP (轻量级进程ID): 每个线程有不同的LWP (221797 和 221798)

  • LWP是线程在内核中的唯一标识符。

  • 主线程的LWP通常等于PID。

  • 其他线程有自己唯一的LWP。

可以直观感受到,线程本质上就是共享相同地址空间和其他资源的"轻量级进程"

那tid是啥呢?我们也可以将tid打印出来看一下,通过 pthread 库中函数 pthread_self 的返回值得到

pthread_self - 获取当前线程ID

函数原型

#include <pthread.h>
pthread_t pthread_self(void);
  • 参数:无
  • 返回值pthread_t 类型,表示当前线程的唯一标识符
  • 错误码:永不失败(总是成功)

线程 ID (pthread_t) 的本质

  • 数据类型
    • 通常为 unsigned long(Linux 实现)
    • 具体类型由操作系统实现定义,可能是整型或结构体 
  • 生命周期
    • 正在运行的线程 ID 唯一
    • 终止后 ID 可被新线程复用(非永久唯一)
  • 作用域:仅在同一进程内有效,跨进程无意义

示例:

#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>

void showid()
{
    printf("tid: %lu\n", pthread_self());
}

void *routine(void *arg)
{
    std::string name = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "我是一个新线程: " << name << ", pid: " << getpid() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void *)"thread-1");
    showid();

    while (true)
    {
        std::cout << "main主线程, pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果:

深入理解两种“线程ID”

在Linux系统中,实际上存在两种不同意义上的“线程ID”,它们处于不同的抽象层次,有不同的用途。

1. pthread_t (POSIX线程ID) - 用户态/库级别ID

  • 来源:由pthread线程库分配和管理。

  • 本质:在Linux的glibc实现中,它确实是一个内存地址。具体来说,它是该线程的线程控制结构(TCB, Thread Control Block)在进程地址空间中的地址

  • 作用域进程内有效。它只在当前进程内有意义,用于在pthread库的函数中标识线程(如pthread_joinpthread_cancel等)。内核完全不知道这个ID的存在。

  • 用途:用于同一进程内的线程间操作和同步。

  • 特点

    • 可移植性差。不同操作系统或不同libc实现可能用不同的方式表示pthread_t(结构体、整数等)。

    • 使用pthread_equal()来比较,不要直接用==(为了可移植性)。

    • 使用pthread_self()获取。

2. LWP (Light Weight Process ID) / TID (Thread ID) - 内核态/系统级别ID

  • 来源:由Linux内核分配和管理。

  • 本质:这是一个pid_t类型的整数,与进程PID属于同一种类型。内核为每一个调度实体(无论是进程还是线程)都分配一个唯一的ID。

  • 作用域系统全局有效。在整个操作系统范围内唯一标识一个调度任务。

  • 用途:用于系统级的监控、调度和调试。toppsperf等工具看到和使用的就是这个ID。

  • 特点

    • 在Linux中,可以通过系统调用gettid()来获取。

    • 主线程的LWP等于进程的PID。

    • 其他线程的LWP是内核分配的新ID。

关键点详解

“pthread_self 得到的这个数实际上是一个地址”

“LWP 得到的是真正的线程ID”

从内核视角看,LWP(由gettid()返回)才是线程的“真实身份”,是调度和资源分配的基本单位。

“主线程和其他线程的栈位置”

对栈位置的描述是Linux线程实现的另一个关键点!

  • 主线程的栈:位于进程虚拟地址空间的栈区域。这个栈是在程序启动时由内核自动设置的,大小通常由系统限制决定(可以用ulimit -s查看)。

  • 其他线程的栈:由pthread库在进程的堆和栈之间的共享区域动态分配。这就是为什么pthread_create可以指定栈大小的原因。

    • 线程栈的分配和管理是pthread库的职责。

    • 当线程退出时,pthread库负责回收这片栈内存。

为什么要设计两层ID?

这种设计体现了优秀的抽象分层思想:

  1. 可移植性:POSIX标准只定义了pthread_t,不关心底层实现。应用程序使用pthread_t可以保证在不同UNIX系统之间的可移植性。

  2. 灵活性:pthread库可以自由选择如何实现和管理线程,比如将TCB结构体放在堆上,并用其地址作为ID。

  3. 效率:用户态的线程操作(如获取自身ID)非常快,无需陷入内核。

  4. 内核简洁性:内核不需要理解复杂的线程库数据结构,它只需要管理好轻量级进程(LWP)的调度即可。

因此:

  • pthread_self()得到的ID是给pthread库用的,用于进程内线程管理。

  • gettid()ps -L看到的LWP是给内核用的,用于系统级任务调度。

  • 两者各司其职,共同构成了Linux强大而灵活的多线程能力。

那既然在内核中,由库来实现和管理线程,那要如何管理起来呢?先描述再组织

pthreads库如何"先描述,再组织"地管理线程

pthreads库虽然运行在用户空间,但它通过精巧的数据结构设计和系统调用封装,实现了完整的线程管理功能。

1. "先描述" - 定义线程控制块(TCB)

pthreads库为每个线程创建一个线程控制块(Thread Control Block, TCB) 数据结构,这就是对线程的"描述"。TCB包含了管理一个线程所需的全部信息:

// 简化的TCB结构示意(实际实现更复杂)
struct pthread {
    /* 线程标识和状态 */
    pthread_t thread_id;        // 线程ID(通常是TCB自身的地址)
    int detach_state;           // 分离状态
    int cancel_state;           // 取消状态
    int cancel_type;            // 取消类型
    
    /* 线程上下文 */
    void *stack_base;           // 栈基地址
    size_t stack_size;          // 栈大小
    void *(*start_routine)(void*); // 线程函数
    void *arg;                  // 线程参数
    void *return_value;         // 返回值
    
    /* 寄存器上下文(用于切换时保存/恢复) */
    void *machine_context;      // 平台相关的寄存器保存区
    
    /* 同步和信号处理 */
    // 各种互斥锁、条件变量、信号处理信息
    
    /* 链接信息 */
    struct pthread *prev, *next; // 用于组织到线程列表中
};

每个TCB就是线程的"身份证"和"档案",完整描述了线程的所有属性和状态。

2. "再组织" - 管理所有TCB

pthread库通过以下数据结构组织所有线程的TCB,实现快速访问与调度:

  1. TCB索引表

    • 全局数组 struct pthread *__thread_list[MAX_THREADS]
    • 通过用户级线程ID(pthread_t)作为下标直接定位TCB(#ref1)
    • 示例:TCB = __thread_list[(unsigned long)pthread_self % MAX_THREADS]
  2. LWP ↔ TCB 映射表

    • 哈希表 hash_map<pid_t LWP, struct pthread* TCB>
    • 用途:内核通过LWP查询TCB(如处理信号时需修改TCB信号掩码)(#ref2)
  3. 线程状态队列

    队列类型 数据结构 用途
    就绪队列 红黑树(按优先级) 用户级调度(配合LWP内核调度)
    等待队列 链表 阻塞在条件变量/互斥锁的线程
    分离线程回收队列 链表 自动回收已终止的分离线程

3. 与内核的协作

虽然pthreads库在用户空间管理线程,但它需要内核的支持来实现真正的并发执行:

  1. 线程创建:当调用pthread_create()时:

    • 库函数分配TCB结构体和线程栈

    • 初始化TCB中的各种字段

    • 将新TCB添加到全局线程列表中

    • 调用clone()系统调用,请求内核创建真正的执行上下文

  2. 线程调度:虽然pthreads库管理线程状态,但实际的调度决策由内核做出。库需要与内核协作处理线程的阻塞、唤醒等状态转换。

  3. 同步原语:互斥锁、条件变量等同步机制虽然在用户空间实现了一部分优化(如futex),但在需要时仍然会通过系统调用进入内核。

线程退出和清理

当线程结束时,pthreads库需要:

  1. 保存线程返回值到TCB中

  2. 如果线程是joinable的,将其标记为已终止但资源尚未回收

  3. 如果是detached的,立即回收TCB和栈空间

  4. 从全局线程列表中移除该TCB

总结:分层抽象的艺术

pthread库的线程管理是用户态与内核态协作的典范

  1. 描述层
    • 通过TCB结构体封装线程全生命周期状态
    • pthread_t 作为TCB指针提供进程内唯一标识
  2. 组织层
    • 全局索引表实现 O(1) 复杂度访问
    • 队列结构管理不同状态线程
  3. 内核桥接
    • 将POSIX API转化为 clone/futex 等系统调用
    • 维护LWP↔TCB映射保证内核操作可定位用户态资源

3. 线程终止

三种线程终止方法详解

1. 从线程函数 return

这是最自然、最推荐的线程终止方式。

工作原理

  • 当线程执行到其启动函数的 return 语句时,线程会正常结束

  • 返回值可以通过 pthread_join 获取

注意事项

  • 主线程中从 main 函数 return 会终止整个进程

  • 返回的指针必须指向全局数据或堆上分配的内存,不能指向线程栈上的局部变量

2. 调用 pthread_exit 终止自己

这种方式允许线程在任何地方主动终止自己,而不必返回到函数开头。

函数原型

void pthread_exit(void *value_ptr);

使用场景

  • 在线程执行的任何地方需要立即退出

  • 当线程需要返回一个值,但无法通过函数返回实现时

重要注意事项

  1. 内存管理value_ptr 不能指向线程栈上的局部变量,因为线程退出后栈会被销毁

  2. 主线程使用:在主线程中调用 pthread_exit 会终止主线程,但其他线程会继续运行,直到所有线程都结束

  3. 清理处理程序:调用 pthread_exit 会执行线程的清理处理程序(通过 pthread_cleanup_push 注册的)

3. 调用 pthread_cancel 取消另一个线程

这种方式允许一个线程请求终止同一进程中的另一个线程。

函数原型

int pthread_cancel(pthread_t thread);

取消机制的工作原理
线程取消不是立即发生的,而是依赖于目标线程的取消状态和类型:

  1. 取消状态(通过 pthread_setcancelstate 设置):

    • PTHREAD_CANCEL_ENABLE:允许取消(默认)

    • PTHREAD_CANCEL_DISABLE:禁止取消

  2. 取消类型(通过 pthread_setcanceltype 设置):

    • PTHREAD_CANCEL_DEFERRED:延迟取消(默认),只在取消点检查取消请求

    • PTHREAD_CANCEL_ASYNCHRONOUS:异步取消,可以在任何时间点被取消

取消点:一些特定的函数调用会成为取消点,如:

  • sleep()usleep()nanosleep()

  • read()write()open()close()

  • pthread_join()pthread_cond_wait()

  • 等等

关键注意事项总结

  1. 返回值的内存管理

    • 无论是通过 return 还是 pthread_exit 返回的值,都必须指向全局数据或堆上分配的内存

    • 绝对不能指向线程栈上的局部变量,因为线程退出后栈会被销毁

  2. 资源清理

    • 线程终止时,系统会自动释放线程特有的资源(如栈空间)

    • 但线程分配的其他资源(如打开的文件、动态分配的内存等)需要程序员显式清理

    • 可以使用 pthread_cleanup_push 和 pthread_cleanup_pop 注册清理函数

  3. 取消的协作性

    • 线程取消是一种协作机制,目标线程必须配合才能被取消

    • 如果线程禁用取消或从不到达取消点,它将无法被取消

  4. 线程分离

    • 如果线程被设置为分离状态(detached),则不需要其他线程调用 pthread_join 来回收资源

    • 分离线程终止后,系统会自动回收其资源


4. 线程等待

为什么需要线程等待?

  1. 资源泄漏防止

    • 线程退出后,其栈空间和线程控制块(TCB)等资源不会自动释放

    • 这些资源会一直占用进程的地址空间,导致"僵尸线程"问题

    • 类似于进程中的僵尸进程,如果不处理,会逐渐耗尽系统资源

  2. 地址空间复用

    • 新创建的线程不会复用已退出线程的地址空间

    • 每次创建新线程都会分配新的栈空间和控制结构

    • 如果不回收旧线程的资源,进程的内存占用会不断增长

  3. 同步需求

    • 主线程可能需要等待工作线程完成特定任务后才能继续执行

    • 线程间需要协调执行顺序,确保数据一致性

  4. 结果获取

    • 工作线程可能需要将执行结果返回给主线程或其他线程

    • pthread_join 是获取线程返回值的标准机制

pthread_join 函数深度解析

函数原型

int pthread_join(pthread_t thread, void **value_ptr);
  • thread:目标线程的 pthread_t 标识符(由 pthread_create 返回)
  • value_ptr:二级指针,用于接收线程退出状态
    • 若传递 NULL,表示忽略退出状态
    • 非 NULL 时,*value_ptr 存储退出信息指针

返回值处理

value_ptr接收的值取决于线程终止方式,形成状态三元组

终止方式 value_ptr指向的内容 典型场景
return 退出 线程函数返回值 return (void*)42;
pthread_exit() pthread_exit 的参数值 pthread_exit((void*)"done")
pthread_cancel() 取消 PTHREAD_CANCELED 宏(-1) pthread_cancel(tid)

📌 关键细节

  • PTHREAD_CANCELED 实际为 (void*)-1,需强转 int 判断
  • 通过 return 和 pthread_exit 返回的值必须位于全局内存或堆中(禁止指向栈变量)

综合示例:

#include <iostream>
#include <string>
#include <cstdlib>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>

void *thread1(void *arg)
{
    printf("thread 1 returning ... \n");
    int *p = (int *)malloc(sizeof(int));
    *p = 1;
    return (void *)p;
}
void *thread2(void *arg)
{
    printf("thread 2 exiting ...\n");
    int *p = (int *)malloc(sizeof(int));
    *p = 2;
    pthread_exit((void *)p);
}
void *thread3(void *arg)
{
    while (true)
    {
        printf("thread 3 is running ...\n");
        sleep(1);
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    void *ret;
    // thread 1 return
    pthread_create(&tid, NULL, thread1, NULL);
    pthread_join(tid, &ret);
    printf("thread return, thread id %lX, return code:%d\n", tid, *(int *)ret);
    free(ret);

    // thread 2 exit
    pthread_create(&tid, NULL, thread2, NULL);
    pthread_join(tid, &ret);
    printf("thread return, thread id %lX, return code:%d\n", tid, *(int *)ret);
    free(ret);

    // thread 3 cancel by other
    pthread_create(&tid, NULL, thread3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED)
        printf("thread return, thread id %lX, return code:PTHREAD_CANCELED\n", tid);
    else
        printf("thread return, thread id %lX, return code:NULL\n", tid);
}

运行结果:

重要注意事项和最佳实践

  1. 线程状态要求

    • 只能对非分离(joinable)状态的线程调用 pthread_join

    • 如果线程处于分离(detached)状态,调用 pthread_join 会失败并返回 EINVAL

  2. 一对一关系

    • 每个线程只能被一个线程 join 一次

    • 多次 join 同一个线程会导致未定义行为

  3. 内存管理责任

    • 通过 pthread_join 获取的返回值内存必须由调用者负责释放

    • 线程不应该返回指向其栈上数据的指针

  4. 错误处理

    • 总是检查 pthread_join 的返回值

    • 常见的错误码:

      • ESRCH:没有找到与给定线程ID对应的线程

      • EINVAL:线程不是可连接状态,或者另一个线程已经在等待此线程

      • EDEADLK:死锁情况,例如线程尝试join自己

  5. 超时处理

    • pthread_join 没有超时机制,会无限期等待

    • 如果需要超时功能,可以考虑使用条件变量或其他同步机制


5. 线程分离

默认情况:可连接线程(Joinable Thread)

  • 新创建的线程默认是可连接的(joinable)

  • 这类线程终止后,必须由其他线程调用 pthread_join 来回收资源

  • 如果不进行 join 操作,线程资源会泄漏,形成"僵尸线程"

分离线程(Detached Thread)

  • 分离线程在终止时会自动释放所有资源

  • 不需要也不能被其他线程 join

  • 适用于不需要获取线程返回值的场景

pthread_detach 函数详解

函数原型

int pthread_detach(pthread_t thread);

参数说明

  • thread:要分离的线程ID

返回值

  • 成功返回 0

  • 失败返回错误码(如 ESRCH 表示线程不存在,EINVAL 表示线程已经是分离状态)

使用方式

1. 创建时分离(推荐)

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);  // 设置分离属性 
pthread_create(&tid, &attr, worker, NULL);  // 直接创建分离线程
pthread_attr_destroy(&attr);
  • 优势:避免运行时状态切换,确保资源安全

2. 其他线程分离目标线程

pthread_create(&tid, NULL, worker, NULL);
pthread_detach(tid);  // 主线程主动分离子线程
  • 适用场景:主线程不关心子线程结果,但需控制分离时机

3. 线程自我分离

void* worker(void* arg) {
    pthread_detach(pthread_self());  // 线程内自行分离 
    // ... 业务逻辑
    return NULL;
}
  • 优势:避免主线程忘记分离,适合动态线程池 

示例:

void *thread_run(void *arg)
{
    pthread_detach(pthread_self());
    printf("%s\n", (char *)arg);
    return NULL;
}
int main(void)
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, thread_run, (void*)"thread1 run...") != 0)
    {
        printf("create thread error\n");
        return 1;
    }
    int ret = 0;
    sleep(1); // 很重要,要让线程先分离,再等待
    if (pthread_join(tid, NULL) == 0)
    {
        printf("pthread wait success\n");
        ret = 0;
    }
    else
    {
        printf("pthread wait failed\n");
        ret = 1;
    }
    return ret;
}

运行结果:

重要注意事项

1. Joinable 和 Detached 是互斥的

  • 一个线程不能同时是可连接和分离的

  • 如果尝试 join 一个已分离的线程,会返回 EINVAL 错误

  • 如果尝试分离一个已分离的线程,也会返回 EINVAL 错误

2. 分离时机

  • 可以在线程创建后的任何时间点分离线程

  • 但最好在知道不需要线程返回值时立即分离

3. 资源回收

  • 分离线程终止时,系统会自动回收其栈空间和线程控制块

  • 但线程分配的其他资源(如打开的文件、动态分配的内存等)仍需程序员负责清理

4. 错误处理

总是检查 pthread_detach 的返回值:

int result = pthread_detach(thread);
if (result != 0) {
    // 处理错误
    if (result == EINVAL) {
        fprintf(stderr, "Thread is already detached or doesn't exist\n");
    } else if (result == ESRCH) {
        fprintf(stderr, "No thread with the ID could be found\n");
    }
}

总结

线程分离是多线程编程中的重要概念,它提供了自动资源回收的机制:

  1. 使用场景:适用于不需要获取线程返回值的后台任务、事件处理等场景

  2. 分离方式

    • 其他线程调用 pthread_detach(thread_id)

    • 线程自我分离:pthread_detach(pthread_self())

    • 创建时指定分离属性

  3. 优势

    • 避免资源泄漏

    • 简化代码,不需要显式调用 pthread_join

    • 提高程序的可维护性

  4. 注意事项

    • 分离后不能再 join

    • 仍需负责清理线程分配的非线程特有资源

    • 总是检查分离操作的返回值


6. 线程封装

代码如下:

Thread.hpp:

#pragma once

#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>

namespace ThreadModlue
{
    static uint32_t number = 1; // 不是原子型的,先不处理
    class Thread
    {
        using  func_t = std::function<void()>; 
    private:
        void Enabledetach()
        {
            std::cout << "线程被分离了" << std::endl;
            _isdetach = true;
        }

        void EnableRunning()
        {
            _isrunning = true;
        }

        // 新线程执行
        static void* Routine(void* args) // 属于类内的成员函数,默认包含this指针!
        {
            Thread* self = static_cast<Thread*>(args);
            self->EnableRunning(); // 修改运行标志位
            if(self->_isdetach)
            {
                self->Detach();
            } 
            pthread_setname_np(self->_tid, self->_name.c_str());
            self->_func(); // 回调处理
            return nullptr;
        }
    public:
        Thread(func_t func)
            : _tid(0), _isdetach(false), _isrunning(false), _ret(nullptr), _func(func)
        {
            _name = "thread-" + std::to_string(number);
        }

        void Detach()
        {
            if (_isdetach)
                return;
            if (_isrunning)
                pthread_detach(_tid);
            Enabledetach();
        }

        bool Start()
        {
            if (_isrunning)
                return false;
            int n = pthread_create(&_tid, nullptr, Routine, this); // 传this指针
            if (n != 0)
            {
                std::cerr << "create thread error : " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                std::cout << "create thread success" << std::endl;
                return true;
            }
        }

        bool Stop()
        {
            if (_isrunning)
            {
                int n = pthread_cancel(_tid);
                if (n != 0)
                {
                    std::cerr << "cancel thread error : " << strerror(n) << std::endl;
                    return false;
                }
                else
                {
                    std::cout << _name << " stop!" << std::endl;
                    return true;
                }
            }
            return false;
        }

        void Join()
        {
            if(_isdetach)
            {
                std::cout << "你的线程已经被分离了, 不能join" << std::endl;
            }
            int n = pthread_join(_tid, &_ret);
            if(n != 0)
            {
                std::cerr << "pthread_join error : " << strerror(n) << std::endl;
                return;
            }
            else
            {
                std::cout << "join success" << std::endl;
            }
        }
        ~Thread() {}

    private:
        pthread_t _tid;
        std::string _name;
        bool _isdetach;  // 分离标志位
        bool _isrunning; // 运行标志位
        void* _ret;
        func_t _func;
    };
}

Main.cc:

#include "Thread.hpp"
#include <unistd.h>
using namespace ThreadModlue;

int main()
{
    Thread t([](){
        while(true)
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            std::cout << "我是一个新线程: " << name << std::endl;
            sleep(1);
        }
    });
    t.Start();
    t.Detach();
    sleep(5);

    t.Stop();

    sleep(5);

    t.Join();
    return 0;
}

运行结果:

注意:

pthread_setname_nppthread_getname_np 是两个用于管理线程名称的非标准函数("_np"后缀表示"non-portable",即不可移植)。这些函数主要适用于Linux和其他类Unix系统,常用于多线程程序的调试与管理。