【Linux】线程库与封装

1、线程等待pthread_join

1.1 为什么需要线程等待

回收线程资源:默认新建线程是joinable,线程退出后,如果没人join回收,线程库内部会保留该线程的退出信息,造成内存泄漏 ,类似僵尸进程,线程已经执行完退出,主线程需要很久再调用pthread_join,仍然可以拿到线程的返回值,说明线程退出信息被库保存了

获得子线程执行结果:主线程等待子线程执行完毕,拿到线程返回的结果

pthread_join是阻塞接口,如果目标线程还没执行完,主线程卡在pthread_join这里,阻塞等待,直到目标线程退出,函数才返回

1.2 函数原型

cpp 复制代码
int pthread_join(pthread_t tid, void **retval);
  • 参数1:tid,要等待的目标线程 ID(pthread_create 输出的 tid)
  • 参数 2:void** retval,二级指针,输出型参数,用来接收线程返回值
  • 返回值:成功返回0;失败返回错误码(比如等待一个已经 detach 的线程,会失败)

子线程函数返回值类型是void* ,这个值保存在线程库维护的线程管理结构中,主线程定义void *ret变量(主线程栈上变量),把&ret(ret 的地址,类型void**)传给pthread_join ,pthread_join内部通过这个二级指针,把线程的返回值拷贝赋值给主线程的 ret 变量,拿到void*后,再强转成想要的类型

1.3 线程返回机制

cpp 复制代码
void* thread_routine(void* args)
  • 返回值是void*,不只是只能返回整数,可以是任意堆上对象的指针:结构体、类对象、任务结果

注意 :不能返回线程栈上局部变量的地址 !线程退出后栈直接销毁,地址变成野指针,推荐在堆上 new/malloc 对象,返回堆地址;主线程拿到后,主线程负责free/delete释放

两种线程退出场景对应的返回值

正常 return 退出 :return (void*)10;,join 拿到的值就是 10,可以强转成 long long,被其他线程调用 pthread_cancel 取消终止 :pthread_join拿到的返回值固定为宏 PTHREAD_CANCELED,值等于(void*)-1

注意 :pthread_join 拿不到线程崩溃的信号信息,只要任意一个线程异常崩溃(除 0、段错误),整个进程直接崩溃,主线程根本没有机会调用 pthread_join 获取退出码

任务传递:

  • 创建线程时:把任务结构体指针作为参数传给子线程
  • 子线程执行任务,填充结果,返回该任务对象指针
  • 主线程pthread_join成功后,才去读取任务结果

这样保证子线程执行完毕之后主线程才访问结果,规避并发读写的竞争问题

1.4 线程中调用exec/fork

  1. 线程内调用 exec 程序替换

exec 替换的是整个进程的地址空间

,会替换整个进程的代码段、页表。调用后,当前进程所有其他线程直接失效崩溃

不推荐在线程直接 exec;如果一定要做:在线程内部fork创建子进程,子进程里调用 exec

  1. 线程内调用 fork

fork 可以在线程内调用。fork 之后,子进程只保留调用 fork 的这一个执行流,其他线程全部消失

子进程是全新独立进程,拥有独立地址空间,子进程内部可以再调用 pthread_create 创建自己的多线程

2、线程分离 pthread_detach

2.1 线程两种状态

1. joinable(默认状态,可连接)

线程退出后,资源(退出信息)不会自动释放,必须调用 pthread_join 回收,否则内存泄漏

2. detached(分离状态)

提前告诉线程库:我不关心这个线程的返回值,不需要 join 。线程一旦退出,线程库自动回收全部线程资源,不需要主线程 join。一旦线程被分离,不能再调用 pthread_join,调用直接失败

2.2 函数原型

cpp 复制代码
int pthread_detach(pthread_t tid);
  • 作用:将目标线程设置为分离态
  • 两种分离方式:
  1. 主线程调用pthread_detach(tid),分离别的线程
  2. 线程内部自己调用pthread_detach(pthread_self());,自己分离自己

2.3 核心

  1. 分离只是修改线程库的状态,不会脱离进程

哪怕线程被 detach,它仍然属于当前进程,共享同一个地址空间。分离线程崩溃,整个进程依旧崩溃

  1. 主线程不能提前退出

主线程一旦 return,整个进程直接终止,所有分离的子线程会被强制杀死,任务没跑完就结束

项目 joinable(默认) detached(分离)
资源回收 必须 pthread_join 手动回收,否则泄漏 线程退出自动回收,无需 join
返回值 pthread_join 可以拿到返回结果 无法获取返回值
pthread_join 调用 允许 调用直接失败
适用场景 需要拿到子线程任务结果,等待任务完成 任务独立,不关心返回,服务器异步任务

2.4 示例

pthread_join 获取返回值

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

void* thread_func(void* arg)
{
    sleep(1);
    return (void*)10; // 线程正常返回
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    void* ret;
    int n = pthread_join(tid, &ret);
    if(n == 0){
        printf("join success, ret = %lld\n", (long long)ret);
    }
    return 0;
}

!image.png

分离线程

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

void* thread_func(void* arg)
{
    pthread_detach(pthread_self()); // 线程自己分离自己
    sleep(2);
    printf("子线程执行完毕\n");
    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    sleep(3); // 主线程不能立刻退出,否则子线程被杀
    printf("main over\n");
    return 0;
}

!image.png

3、C++面向对象封装pthread

目标:封装成Thread类,使用风格:

cpp 复制代码
Thread t(回调任务);
t.start(); // 启动线程,创建内核LWP
t.join();  // 等待回收

3.1 类成员设计(thread.hpp)

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

namespace thread_module {
// 线程状态枚举
enum class ThreadStatus {
    NEW,     // 对象创建,内核线程未创建
    RUNNING, // 线程正在运行
    STOP     // 线程结束
};

class Thread {
private:
    std::string _name;
    pthread_t _tid;                // pthread库线程id
    ThreadStatus _status;
    bool _joinable;                // 是否可以join

    using Callback = std::function<void()>;
    Callback _cb; // 用户传入的任务回调

    // ==========【最重要难点】static 静态入口函数 ==========
    // pthread_create要求入口函数签名:void* (*)(void*)
    // 普通成员函数自带隐藏this指针,参数变成 (Thread* this, void* arg),类型不匹配!
    static void* thread_routine(void* args);

    void setRunning();  // 设置状态为running
public:
    // 构造:接收用户任务回调
    Thread(Callback cb);
    ~Thread();

    bool start();
    bool join();
    bool detach();
    bool stop(); // pthread_cancel强制取消线程

    // 调试打印接口
    void printInfo();
    static std::string statusToString(ThreadStatus s);
};
}

为什么线程入口必须加static?

  1. 普通成员函数:编译器自动添加第一个隐藏参数this,签名等价:

void* thread_routine(Thread* this, void* args)

pthread_create只接受void* (*)(void*),参数数量不匹配 → 编译报错

  1. static 静态成员函数 :属于类,不属于对象,没有 this 指针 ,签名完美匹配void* (*)(void*)
  2. 解决访问非静态成员:
  • pthread_create的第四个参数传this(当前对象指针)
  • 在thread_routine内部把void* args强转回Thread* self
  • 通过self->访问对象的_cb、_status等成员
cpp 复制代码
void* Thread::thread_routine(void* args)
{
    Thread* self = static_cast<Thread*>(args);
    self->setRunning();
    // 设置内核线程名字
    pthread_setname_np(self->_tid, self->_name.c_str());

    // 执行用户传入的任务回调
    self->_cb();

    // 任务执行完毕,修改状态为stop
    self->_status = ThreadStatus::STOP;
    return nullptr;
}

3.2 对外接口逻辑

  1. start()
  • 调用pthread_create(&_tid, nullptr, thread_routine, this)
  • 创建成功,内核 LWP 诞生;对象创建(构造函数)≠ 内核线程创建,对象和线程生命周期解耦
  • 线程内部自己把状态改为RUNNING,不要主线程直接修改状态,避免竞态
  1. detach() 分离
  • 前置条件:线程必须是RUNNING并且_joinable=true
  • 调用pthread_detach(_tid),_joinable = false
  • 分离后:线程退出自动释放 TCB 资源,禁止 join,join 会直接失败

注意 :start()之后立刻detach,主线程执行速度快,新线程还没被内核调度、状态还没改成 RUNNING,detach 判断条件失败,分离失败。解决:pthread_create成功拿到 tid 就代表内核线程已经存在,可以直接修改状态,不要等子线程自己改状态再 detach

  1. stop()强制终止
  • 前置:状态是RUNNING
  • 调用pthread_cancel(_tid),将状态置为STOP
  1. join
  • 前置:_joinable == true
  • pthread_join(_tid, nullptr),主线程阻塞等待线程结束,回收线程资源
  • join 成功后标记线程状态为STOP

3.3 线程传参方案

方案1:模板传参

把Thread做成模板类,构造接收一个参数,回调接收该参数。缺点:只能传一类参数

方案2:任务类 + lambda捕获

核心思想:任务本身封装所有参数,线程回调无参,用 lambda 捕获任务对象,解耦,不用模板可变参数

  1. 写任务类,任务内保存成员变量,提供execute()执行函数:
cpp 复制代码
struct AddTask {
    int _x, _y;
    int _res;
    AddTask(int x, int y):_x(x),_y(y),_res(0){}
    void execute() {
        _res = _x + _y;
    }
};
  1. 创建任务对象,lambda捕获任务,传给Thread:
cpp 复制代码
AddTask task(10,20);
Thread t(&{
    task.execute();
});
t.start();
t.join();

优点:

  • 想要多少参数,就在任务类写多少成员;不需要修改线程封装代码
  • 线程回调永远无参,线程类不需要关心任务是什么,线程和任务解耦

3.4 批量管理多线程:先描述再组织

  • 我们Thread类就是对线程的描述;
  • 使用容器std::vector<Thread>存放多个线程对象,完成组织
cpp 复制代码
std::vector<Thread> threads;
// 创建10个线程对象
for(int i=0;i<10;i++){
    threads.emplace_back([](){
        // 任务逻辑
    });
}
// 统一全部启动
for(auto& t : threads) t.start();
// 统一等待回收
for(auto& t : threads) t.join();

拓展脑洞:可以做用户态简易调度器,用队列保存线程,主线程循环 stop、restart 切换执行

3.5 线程内fork

  • 线程可以调用fork()创建子进程
  • fork之后,只有调用 fork 的那个线程会在子进程中保留,其他线程全部消失
  • 多线程环境 fork 极易造成死锁、资源混乱,工程尽量避免

4、抢票demo

4.1 代码逻辑

cpp 复制代码
int tickets = 10000; // 全局共享资源

void get_ticket()
{
    std::string name;
    pthread_getname_np(pthread_self(), &name[0], 64);
    while(true)
    {
        if(tickets > 0)
        {
            usleep(1000); // 模拟抢票业务耗时
            printf("%s get ticket: %d\n", name.c_str(), tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    Thread t1(get_ticket);
    Thread t2(get_ticket);
    Thread t3(get_ticket);
    Thread t4(get_ticket);
    t1.start();
    t2.start();
    t3.start();
    t4.start();
    t1.join();t2.join();t3.join();t4.join();
    return 0;
}

运行现象:票数会抢到负数

4.2 为什么 tickets 变成负数

核心:tickets>0 判断 和 tickets-- 不是原子操作! 一条 C++ 语句会被编译成多条汇编指令,线程随时会被操作系统调度切走(上下文切换)

拆解tickets--汇编三步:

  1. 把内存tickets读取到 CPU 寄存器
  2. 寄存器内值 -1
  3. 把寄存器的值写回内存tickets

初始tickets = 1

  1. 线程 1:执行if(tickets>0),把内存的1读到寄存器,判断1>0 → true,正要进入循环体,时间片耗尽,线程 1 被操作系统切走
  • 线程上下文保存:寄存器里的值 = 1,PC 保存到 usleep 这一行
  • 内存tickets仍然是 1,没有执行 tickets--
  1. 调度器切换到线程 2:
    • 线程 2 读取内存tickets=1,判断>0成立,进入代码块,usleep 休眠
  2. 线程 2 休眠,切回线程 1 继续运行:
    • 线程 1 上下文恢复,寄存器里的值还是 1,执行打印,然后tickets--,内存变成 0
  3. 线程 2 被唤醒,执行打印,tickets--,内存变成 1

结论:

  • tickets是共享资源 ;访问tickets的if判断、tickets--代码段叫做临界区
  • 多个执行流同时进入临界区 访问临界资源,就会产生数据竞争,数据不一致
  • 显示器标准输出 stdout 也是共享资源,多线程 printf 不加保护会出现打印内容穿插乱码。

4.3 核心概念

  1. 共享资源:多个执行流都可以访问的资源(全局变量、文件、显示器)
  2. 临界资源 :需要保护的共享资源
  3. 临界区 :代码片段,访问临界资源的那一段代码
  4. 互斥:任何时刻,最多只有一个执行流进入临界区访问临界资源。
  5. 原子性 :操作不可拆分,一旦开始执行,不会被线程切换打断。tickets--不是原子操作。
相关推荐
lwx9148521 小时前
Deepin操作系统
linux·服务器·网络
先知后行。1 小时前
进程间通信(IPC)
linux·运维·服务器
weixin_571667412 小时前
Windows 通过 SSH 反向隧道向 Ubuntu 转发代理
linux·运维·windows·代理
乔旭伟2 小时前
CentOS 停更迁移实战:vault 源配置、目标选型与迁移工程清单
linux·运维·centos
鬼手点金3 小时前
ReAct 循环
linux·服务器·前端·javascript·react.js·前端框架
皓月盈江3 小时前
Linux Debian下STM32CubeProgrammer串口连接失败解决方案
linux·stm32·debian·串口下载·stm32f103c8t6·usb转ttl·stm32cubepro
远方16094 小时前
108-ORA-00600 [kdsgrp1-qetlbr] 内部错误排查与 RMAN 块级恢复
大数据·linux·数据库·oracle
yunwei374 小时前
eBPF 实例教程:BPF 动态指针处理可变长度数据
linux·后端·性能优化
TDTC_XQ4 小时前
模板类的写法
linux·c++