【Linux系统】线程控制

1. POSIX线程库 (pthreads)

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

核心特性

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

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

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

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

为什么需要 -lpthread 选项?

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

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

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

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


2. 线程创建

函数原型与核心机制

cpp 复制代码
#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:没有权限设置指定的调度策略或参数

示例:

cpp 复制代码
#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;
}

运行结果:

bash 复制代码
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 选项:打印线程信息

bash 复制代码
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

函数原型

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

线程 ID (pthread_t) 的本质

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

示例:

cpp 复制代码
#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_join, pthread_cancel等)。内核完全不知道这个ID的存在。

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

  • 特点

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

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

    • 使用pthread_self()获取。

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

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

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

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

  • 用途 :用于系统级 的监控、调度和调试。top, ps, perf等工具看到和使用的就是这个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包含了管理一个线程所需的全部信息:

cpp 复制代码
// 简化的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 终止自己

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

函数原型

cpp 复制代码
void pthread_exit(void *value_ptr);

使用场景

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

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

重要注意事项

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

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

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

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

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

函数原型

cpp 复制代码
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_pushpthread_cleanup_pop 注册清理函数

  3. 取消的协作性

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

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

  4. 线程分离

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

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


4. 线程等待

为什么需要线程等待?

  1. 资源泄漏防止

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

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

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

  2. 地址空间复用

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

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

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

  3. 同步需求

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

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

  4. 结果获取

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

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

pthread_join 函数深度解析

函数原型

cpp 复制代码
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 判断
  • 通过 returnpthread_exit 返回的值必须位于全局内存或堆中(禁止指向栈变量)

综合示例:

cpp 复制代码
#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 函数详解

函数原型

cpp 复制代码
int pthread_detach(pthread_t thread);

参数说明

  • thread:要分离的线程ID

返回值

  • 成功返回 0

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

使用方式

1. 创建时分离(推荐)

cpp 复制代码
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. 其他线程分离目标线程

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

3. 线程自我分离

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

示例:

cpp 复制代码
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 的返回值:

cpp 复制代码
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:

cpp 复制代码
#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:

cpp 复制代码
#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系统,常用于多线程程序的调试与管理。


相关推荐
AlfredZhao7 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户97183563346613 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪15 小时前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush41 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩1 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈1 天前
Unix 与 Linux 异同小叙
linux·服务器·unix
凡人叶枫1 天前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
2601_961875241 天前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant