linux线程控制

POSIX 线程库(简称 pthread)是 UNIX/Linux 系统下遵循 POSIX 标准 的多线程编程接口,提供了一套跨平台的线程创建、同步和管理机制。

1.线程创建

1.1.pthread_create

pthread_create()是 POSIX 线程库中用于创建新线程的核心函数,其功能类似于进程中的fork(),但更轻量级。

返回值:成功时返回 0,失败时返回错误码(非 errno,需用 strerror() 转换)。

参数解析

thread:输出参数,用于保存新线程的唯一标识符(线程ID)。
attr:线程属性(如栈大小、调度策略),传 NULL 表示使用默认属性。
start_routine:线程入口函数,格式必须为 void *func(void *args)。
arg:传递给 start_routine 的参数,需强制转换为 void* 类型。

传入的start_routine如果是成员函数则必须是静态成员函数:

 - start_routine 参数的类型是 void* (*start_routine)(void*),即一个 “参数为 void*、返回值为 void* 的函数指针”。

 - C++ 的非静态成员函数会隐式包含一个 this 指针(指向类实例自身),其函数签名void* (Thread<T>* this, void* args) 与 start_routine 不兼容

 - 静态成员函数属于类本身,而非类的某个实例,它不包含 this 指针,函数签名为 void* Routine(void*),刚好匹配 pthread_create 对 start_routine 的要求

线程被创建好后,新线程要被主线程等待,类似僵尸进程的问题。

1.2.pthread_join

pthread_join 是 POSIX 线程库中用于线程同步和资源回收的关键函数,主要作用是阻塞当前线程,直到目标线程终止,并获取其返回值。

pthread_t thread:目标线程的ID(需等待的线程)
void **retval:输出参数:存储目标线程的返回值(可设为NULL)

    成功返回 0,失败返回错误码(非 errno)。

    #include <iostream>
    #include <string>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    
    void showtid(pthread_t tid)
    {
        printf("tid: 0x%lx\n", tid);
    }
    void *routine(void *args)
    {
        std::string name = static_cast<const char*>(args);
        int cnt = 5;
        while(cnt--)
        {
            std::cout << "我是一个新线程:" << name << std::endl;
        }
        return nullptr;
    }
    int main()
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, routine, (void*)"thread-1");
    
        showtid(tid);
    
        pthread_join(tid, nullptr);
        return 0;
    }
    

    1.3.pthread_self

    pthread_self() 是 POSIX 线程库中用于获取当前线程唯一标识符(线程ID)的函数,其功能类似于进程中的 getpid()。

    #include <iostream>
    #include <string>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    int flag = 1000;
    void showtid(pthread_t tid)
    {
        printf("tid: 0x%lx\n", tid);
    }
    
    std::string FormatId(pthread_t id)
    {
        char tid[64];
        snprintf(tid, sizeof(tid), "0x%lx", id);
        return tid;
    }
    
    void *routine(void *args)
    {
        std::string name = static_cast<const char*>(args);
        pthread_t id = pthread_self(); 
        int cnt = 5;
        while(cnt--)
        {
            sleep(1);
            std::cout << "我是一个新线程:" << name << "我的id是:" << FormatId(id) << std::endl;
            flag++;
        }
        return (void*)100;
    }
    int main()
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, routine, (void*)"thread-1");
    
        showtid(tid);
    
        int cnt = 5;
        while(cnt--)
        {
            std::cout << "我是主线程:" << "我的id'是:" 
            << FormatId(pthread_self()) << ", flag: " << flag << std::endl;
            sleep(1);
        }
    
        void *ret = nullptr;
        pthread_join(tid, &ret);
        std::cout << "ret: " << (long long)ret << std::endl;
        return 0;
    }

    可以看到两个线程共享同一份资源

    两个线程都调用了这个函数,这是一个可重入函数 

    std::string FormatId(pthread_t id)
    {
        char tid[64];
        snprintf(tid, sizeof(tid), "0x%lx", id);
        return tid;
    }

    给新线程传递的参数和返回值可以是任意类型

    class Task
    {
    public:
        Task(int a, int b)
        : _a(a)
        , _b(b)
        {}
        int Execute()
        {
            return _a + _b;
        }
        ~Task(){}
    private:
        int _a;
        int _b;
    };
    class Result
    {
    public:
        Result(int result) : _result(result){}
    
        int GetResult() { return _result;}
        ~Result(){}
    private:
        int _result;
    };
    
    void *routinue(void *args)
    {
        Task *t = static_cast<Task*>(args);
        sleep(1);
        Result *res = new Result(t->Execute());
        sleep(1);
        return res;
    }
    
    int main()
    {
        pthread_t tid;
        Task *t = new Task(10, 20);
        pthread_create(&tid, nullptr, routinue, (void*)t);
    
        Result *ret = nullptr;
        pthread_join(tid, (void **)&ret);
        int n = ret->GetResult();
    
        std::cout << "进程结束,退出码:" << n << std::endl;
    
        delete t;
        delete ret;
    
        return 0;
    }

    2.线程终止

    如果需要只终止某个线程而不终止整个进程,可以有三种方法。

    1. 从线程函数中 return。这种方法对主线程不适用,从 main 函数 return 相当于调用 exit。

    2. 线程可以调用 pthread_ exit 终止自己。

    参数等价于return void*

    3. 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程。

    pthread_cancel 用来取消同一个进程中的线程

    返回值:成功返回0,失败返回错误码 

    #include <iostream>
    #include <string>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    
    class Task
    {
    public:
        Task(int a, int b)
        : _a(a)
        , _b(b)
        {}
        int Execute()
        {
            return _a + _b;
        }
        ~Task(){}
    private:
        int _a;
        int _b;
    };
    class Result
    {
    public:
        Result(int result) : _result(result){}
    
        int GetResult() { return _result;}
        ~Result(){}
    private:
        int _result;
    };
    
    void *routinue(void *args)
    {
        Task *t = static_cast<Task*>(args);
        sleep(100);
        Result *res = new Result(t->Execute());
    
        //return res;
        pthread_exit(res); //与return res等价
    }
    
    int main()
    {
        pthread_t tid;
        Task *t = new Task(10, 20);
        pthread_create(&tid, nullptr, routinue, (void*)t);
    
        sleep(3);
        pthread_cancel(tid);
        std::cout << "新线程被取消" << std::endl;
    
        void *ret = nullptr;
        pthread_join(tid, &ret);
        std::cout << "新线程结束,退出码:" << (long long)ret << std::endl;
    
        return 0;
    }
    

    线程如果被退出,退出结果是-1

    thread 线程以不同的方式终止,通过 pthread_join 得到的终止状态是不同的,总结如下:

    1. 如果 thread 线程通过 return 返回,value_ptr 所指向的单元里存放的是 thread 线程函数的返回值。

    2. 如果 thread 线程被别的线程调用 pthread_cancel 异常终止,value_ptr 所指向的单元里存放的是常数 PTHREAD_CANCELED

    3. 如果 thread 线程是自己调用 pthread_exit 终止的,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数

    4. 如果对 thread 线程的终止状态不感兴趣,可以传 NULL 给 value_ptr 参数。

    3.分离线程(pthread_detach)

    默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则,
    无法释放资源,从而造成系统泄漏。 如果不关心线程的返回值,join是一种负担,这个时候,我们
    将线程设置为分离状态,当线程退出时,自动释放线程资源。 

    pthread_detach 用于将线程设置为 分离状态(detached),使得线程结束时自动释放资源,而无需调用 pthread_join。 

    void *routinue(void *args)
    {
        pthread_detach(pthread_self());
        std::cout << "线程被分离" << std::endl;
    
        int cnt = 5;
        while(cnt--)
        {
            sleep(1);
            std::cout << "我是新进程" << std::endl;
        }
    
        return nullptr;
    }
    
    int main()
    {
        pthread_t tid;
        Task *t = new Task(10, 20);
        pthread_create(&tid, nullptr, routinue, (void*)t);
    
        int cnt = 5;
        while(cnt--)
        {
            sleep(1);
            std::cout << "我是主进程" << std::endl;
        }
    
        int n = pthread_join(tid, nullptr);
        if(n == 0)
        {
            std::cout << "pthread_join sucess: " << std::endl;
        }else
        {
            std::cout << "pthread_join fail: " << std::endl;
        }
        
        return 0;
    }
    

    4.设置线程名称

    (1)pthread_setname_np(设置线程名称)

    功能:为指定线程设置系统可见的名称。

    int pthread_setname_np(pthread_t thread, const char *name);

    thread:要设置名称的线程句柄(pthread_t 类型)。
    name:线程名称,长度限制通常为 16 个字符(含终止符 \0),超出会被截断。

    返回值:成功返回 0,失败返回错误码。
    作用:设置后可通过 top -H、ps -T、gdb 等工具查看线程名称,方便调试多线程程序。

    (2)pthread_getname_np(获取线程名称)

    功能:获取指定线程的名称。

    int pthread_getname_np(pthread_t thread, char *name, size_t len);

    name:用于存储线程名称的字符数组。
    len:name 数组的长度(需保证至少 16 字节以容纳名称)。

    返回值:成功返回 0,失败返回错误码。

    5.用户态线程(pthread)与 struct pthread

    - struct pthread(TCB,Thread Control Block)
      - 是 pthread 库在 用户态 维护的线程管理结构,包含:

    struct pthread {
        void* ret;       线程返回值(通过 return 或 pthread_exit 写入)
        void* stack;     线程独立栈的地址
        size_t stack_size;  栈大小
        pthread_attr_t attr;    线程属性(如分离状态、调度策略)
        pid_t tid;       内核 LWP(通过 gettid() 获取)
        其他状态(如取消标志、锁等)
    };

      - pthread_create 会动态分配 struct pthread 对象,并返回其地址(即 pthread_t,本质是用户态句柄)。使用 pthread_create,会在库中创建TCB,在内核中创建轻量级进程(调用系统调用clone)

    - 线程退出时

      - struct pthread 也就是TCB,有一个属性void *(ret),当线程执行 return 或调用 pthread_exit()时,就会把返回值写入 ret 中。  

      - 此时线程虽然执行结束,但这个数据块并没有被释放,所以需要 pthread_join,并且需要传入该线程的 tid 作为参数已找到该线程对应的数据块

    6. 内核态线程(LWP)与 task_struct

    clone() 系统调用:pthread_create 底层通过 clone() 创建 内核线程(LWP)。

    clone(
        fn:线程函数
        stack:用户态栈地址
        flags:CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD,
        args:传递给线程的参数
    );
    CLONE_THREAD:新线程共享相同的 PID(属于同一线程组).
    CLONE_VM:共享地址空间(同一进程)。

    task_struct(PCB):内核为每个 LWP 维护一个 `task_struct`,包含:

        struct task_struct {
            pid_t          pid;      // 线程组 ID(用户态看到的 PID)
            pid_t          tgid;     // 等同于 pid(主线程的 PID)
            pid_t          tid;      // 内核 LWP(唯一线程 ID)
            struct mm_struct *mm;    // 内存管理(共享同一进程的 mm)
            // 调度相关:时间片、优先级、上下文(registers、FPU state)
        };

    pthread_t vs LWP

    概念	        用户态(pthread 库)	        内核态(Linux 内核)
    线程标识符	pthread_t(TCB 地址)	    tid(LWP,通过 gettid() 获取)
    调度单位	    无(依赖内核 LWP)	        task_struct(LWP)
    栈管理	    用户态分配栈(stack 属性)	    内核映射用户栈到进程地址空间

    线程创建流程:
      1. 用户调用 pthread_create。  
      2. pthread 库分配 struct pthread(TCB)和用户栈。  
      3. 调用 clone() 创建内核线程(LWP),共享进程地址空间。  
      4. 新线程从用户态启动函数(fn)开始执行。  

    线程退出流程:
      1. 线程调用 return 或 pthread_exit,返回值写入 TCB 的 ret。  
      2. 内核线程(LWP)退出,但 struct pthread 仍保留(供 pthread_join 读取)。  
      3. pthread_join 回收 TCB 和用户栈资源。  

    pthread_t 到底是什么?
    - 在 glibc 中,pthread_t 是 struct pthread(用户态 TCB )的地址。  
    - 其他实现(如 musl libc)可能直接使用 tid(LWP),但 Linux 主流实现是 TCB 地址。

    总结
    - 用户态 pthread 库 管理 struct pthread(TCB),负责线程创建、属性、返回值等。  
    - 内核态 task_struct 管理 LWP,负责调度、时间片、上下文切换。  
    - pthread_t 是用户态句柄,LWP 是内核调度单位,二者通过 clone() 协作。  
    - 务必调用 pthread_join 或 pthread_detach 避免资源泄漏!

    7.独立栈

    1. 主线程(进程)的栈

    - 分配方式:
      - 通过 fork() 创建时,继承父进程的栈空间地址,采用 写时拷贝(CoW)机制。
      - 栈空间可动态增长(由内核自动扩展),直至达到上限(默认通常 8MB)。

    - 溢出行为:
      - 访问未映射的栈地址时,内核尝试扩展栈;若超出上限则触发 段错误(SIGSEGV)。

    - 特点:
      - 向下增长:栈指针从高地址向低地址移动。
      - 唯一允许“试探性”访问未映射页而不立即报错的内存区域(直到触及硬限制)。

    2. 子线程(pthread)的栈
    - 分配方式:
      - 通过 pthread_create() 创建时,由 glibc 调用 mmap 在进程的 文件映射区(共享区)分配固定大小的栈空间。

      mem = mmap(NULL, size, PROT_READ | PROT_WRITE, 
                 MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

      - 默认大小通常为 8MB,可通过 pthread_attr_setstacksize() 自定义。

    - 溢出行为:
      - 不可动态增长:栈耗尽时直接访问非法内存,立即触发段错误。

    - 特点:
      - 固定大小:由 mmap 预先分配,无 CoW 机制。
      - 非严格私有:虽为线程私有,但因共享进程地址空间,其他线程可通过指针非法访问(需同步控制)。独立的栈,是指其他线程不知道该栈的地址,但是可以访问里面的内容,因为共享地址空间。

    线程独立的上下文:有独立的PCB(内核)+ TCB(用户层,pthread库内部)

    独立的栈:每个线程线程都有自己独立的栈,要么是进程的,要么是库中创建进程是mmap申请出来的

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值