云计算百科
云计算领域专业知识百科平台

Re:Linux系统篇(五十一)线程篇 · 四:深入pthread库:创建、等待、取消、分离一篇讲透

在这里插入图片描述

◆ 博主名称: 小此方-CSDN博客

大家好,欢迎来到小此方的博客。

⭐️Linux系列个人专栏:
【主题曲】Linux

⭐️此方的GitHub:
github_此方

⭐️
Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录

  • 概要&序論
  • 一、认识 pthread 线程库
    • 1.1 为什么需要 pthread 库?
    • 1.2 多线程的层状封装关系
      • 1.2.1 底层到用户层的调用链路
      • 1.2.2 C++11 等高级语言的跨平台实现
  • 二、线程的控制接口
    • 2.1 线程创建与线程等待
      • 2.1.1 线程创建:pthread_create详解
      • 2.1.2 线程等待:pthread_join
      • 2.1.3 获取自身线程 ID:pthread_self
      • Tips:线程ID相关误区
      • 2.1.4线程创建/等待demo代码
      • 2.1.5线程处理任务demo代码
    • 2.2 线程终止与线程取消
      • 2.2.1 线程终止的方式
      • 2.2.2 线程取消:pthread_cancel
    • 2.3 线程分离:pthread_detach
      • 2.3.1 为什么需要线程分离?
      • 2.3.2 pthread_detach 接口
    • 2.4 Tips:一些值得思考的细节问题
      • 2.4.1 新线程能不能开更小的线程?
      • 2.4.2 新线程能不能把主线程取消掉?
    • 2.5 线程终止与线程分离demo
      • 2.5.1线程分离demo
      • 2.5.2线程终止demo

概要&序論

  Hello大家好,我是此方。在上一篇文章中,我们深入了解了线程的基本概念以及线程与进程之间的关系。本文将正式进入Linux多线程编程实战,学习pthread线程库提供的核心控制接口,包括线程创建、等待、退出、取消以及线程分离。通过这些接口,我们将进一步理解线程完整生命周期背后的系统机制。

一、认识 pthread 线程库

1.1 为什么需要 pthread 库?

  在操作系统层面,Linux 并没有传统意义上真正独立的“线程”概念。内核中只有轻量级进程(LWP, Light Weight Process),内核主要通过 clone 或 vfork 等系统调用来实现地址空间共享的进程创建。   但对于用户来说,开发者只关心“线程”这一抽象概念。为了弥补用户需求与底层实现的鸿沟,Linux 提供了 pthread 库(即 POSIX 线程库,属于原生线程库)。它把底层的轻量级进程系统调用封装起来,直接向用户暴露一套标准的线程创建与控制接口。

1.2 多线程的层状封装关系

  Linux 系统的线程实现在用户层(用户级线程),用户无需关心底层的 LWP 细节。它的整体层状关系与跨平台封装逻辑如下:

1.2.1 底层到用户层的调用链路

  • 用户层:不同的语言封装了不同的线程库。比如C++11的并发支持库。
  • 原生线程库层(pthread):对底层接口进行封装。
  • 操作系统层(OS):内核提供 clone 等系统调用,创建与父进程共享地址空间的轻量级进程。

在这里插入图片描述

1.2.2 C++11 等高级语言的跨平台实现

  像 C++11 中的 std::thread 能够实现跨平台可移植性,其底层也是基于条件编译进行的二次封装:

  • Linux 环境下:std::thread 本质上就是对 pthread 库 的进一步封装。
  • Windows 环境下:std::thread 则是对 Windows 系统的原生创建线程接口进行的封装。

//举例,C++11的并发支持库
void hello(){
while (true)
std::cout << "新线程: hello world" << std::endl;
}
int main(){
std::thread t(hello);
while (true)
std::cout << "我是主线程…" << ", pid: " << getpid() << std::endl;
t.join();
return 0;
}

  在未来我也会在现在C++专栏中讲解C++11的并发支持库的相关内容。

二、线程的控制接口

2.1 线程创建与线程等待

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 “pthread_” 打头的
  • 要使用这些函数库,要通过引入头文 <pthread.h>
  • 链接这些线程函数库时要使用编译器命令的 “-lpthread” 选项

2.1.1 线程创建:pthread_create详解

  函数原型:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

  参数详解:

  • thread:输出型参数,用于带回创建成功的线程 ID(tid)。注意:线程 tid 是 pthread 库维持的概念,不要直接与内核的 LWP 概念混淆。
  • attr:用于设置线程属性,通常直接传入 nullptr 表示使用默认属性。
  • start_routine:函数指针,即新线程要执行的入口函数。
  • arg:传递给 start_routine 函数的参数。

  返回值:

  • 成功时返回 0。
  • 失败时返回 错误码(error number),且 *thread 的内容未定义。
  • main函数结束,代表主线程结束,一般也代表进程结束
  • 新线程对应的入口函数,运行结束,代表当前线程运行结束
    • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
    • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。
    • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小。

    2.1.2 线程等待:pthread_join

      新线程创建后,必须被主线程或其它线程等待,否则会造成类似于僵尸进程的问题,导致内存泄露。

    函数原型:

    int pthread_join(pthread_t thread, void **retval);

    参数与作用:

    • thread:需要等待的目标线程 ID(即 pthread_create 获取的 thread)。
    • retval:输出型参数,用来接收目标线程退出时的返回值。

    为什么在join的时候,没有见到异常相关的字段呢

    • 等待的目标线程如果发生异常,会导致整个进程崩溃退出(包括 main 线程),因此在 join 时试图捕获线程异常没有任何意义。
    • join 接口的设计完全是基于线程正常跑完的情况。它不需要处理异常信号,处理异常信号是属于进程层面的话题!

      调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  • 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  • 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
  • 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  • 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
  • 在这里插入图片描述

    2.1.3 获取自身线程 ID:pthread_self

      函数原型:

    pthread_t pthread_self(void);

    作用: 返回调用线程自身的线程 ID(tid)。

    Tips:线程ID相关误区

      很多人对线程id有所误解,我们打印一个线程ID看看,它其实不是LWP。

    在这里插入图片描述   打印出来的 tid 是一个 pthread_t 类型的变量,指代的是调用 pthread_self 函数的线程的“ID”。   怎么理解这个“ID”呢?这个“ID”是 pthread 库给每个线程定义的进程内唯一标识,是 pthread 库维持的。   由于每个进程有自己独立的内存空间,故此“ID”的作用域是进程级而非系统级(内核不认识)。 其实 pthread 库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯一的“ID”来唯一标识这个线程。

    在这里插入图片描述   LWP 是什么呢?LWP 得到的是真正的线程ID。之前使用 pthread_self 得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。   在 ps -aL 得到的线程ID,有一个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。(这块儿我们现在提一嘴,放在后面讲)

    2.1.4线程创建/等待demo代码

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>

    void* thread_run(void* arg) {
    const char* name = (const char*)arg;
    for (int i = 0; i < 3; i++) {
    std::cout << name << " running, tid: " << pthread_self() << std::endl;
    sleep(1);
    }
    return (void*)100;
    }

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

    void* ret = nullptr;
    pthread_join(tid, &ret);

    std::cout << "thread exit code: " << (long long)ret << std::endl;
    return 0;
    }

    在这里插入图片描述

      给线程传递的参数和返回值,可以是任意类型,于是自然可以是对象。

    2.1.5线程处理任务demo代码

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>
    #include <string>
    #include <vector>
    #include<functional>
    using Task = std::function<void(void)> ;
    class TaskManager{
    private:
    std::vector<Task> Taskv;
    public:
    TaskManager(){};
    ~TaskManager(){};
    void Addtask(const std::function<void(void)>& task){
    Taskv.push_back(task);
    }
    Task Gettask(int i){
    return Taskv[i];
    }
    };

    void *Routine(void *args)
    {
    Task* task = (Task*) args ;
    (*task)();
    delete task ;
    // for (int i = 0; i < 10; i++)
    // std::cout << "我是一个线程" << "我的名字是" << args << "我的tid是" << pthread_self() << std::endl;
    return (void *)100;
    }
    void func_1() { std::cout << "线程 [" << pthread_self() << "] 正在执行:网络任务" << std::endl; }
    void func_2() { std::cout << "线程 [" << pthread_self() << "] 正在执行:数据库任务" << std::endl; }
    void func_3() { std::cout << "线程 [" << pthread_self() << "] 正在执行:系统任务" << std::endl; }
    void func_4() { std::cout << "线程 [" << pthread_self() << "] 正在执行:重要任务" << std::endl; }
    void func_5() { std::cout << "线程 [" << pthread_self() << "] 正在执行:关键任务" << std::endl; }
    int main()
    {
    TaskManager taskmag ;
    taskmag.Addtask(func_1);
    taskmag.Addtask(func_2);
    taskmag.Addtask(func_3);
    taskmag.Addtask(func_4);
    taskmag.Addtask(func_5);

    std::vector <pthread_t> pvtion ={0};
    for (int i = 0; i < 5; i++)
    {
    Task* task = new Task(taskmag.Gettask(i));
    int n = pthread_create(&pvtion[i], NULL, Routine, task);
    if (n != 0)
    std::cerr << "pthread_create error" << std::endl;
    }
    for (int i = 0; i < 5; i++)
    {
    void *ret = 0;
    pthread_join(pvtion[i], &ret);
    std::cout << (long)ret << std::endl;
    }
    return 0;
    }

    2.2 线程终止与线程取消

    2.2.1 线程终止的方式

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

  • 入口函数 return:线程入口函数运行结束并返回,代表当前线程运行结束(注意:不能在线程中使用 exit(),因为 exit() 是用来终止整个进程的!)。
  • 使用 pthread_exit 函数:终止调用它的线程本身。
  • 被其他线程取消:通过 pthread_cancel 终止。
  • pthread_exit 函数原型:

    #include <pthread.h>

    void pthread_exit(void *retval);

    • retval:线程退出的返回值(同 return retval;),可以被 pthread_join 获取。

      需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

    2.2.2 线程取消:pthread_cancel

    函数原型:

    #include <pthread.h>

    int pthread_cancel(pthread_t thread);

    参数与细节:

    • thread:要取消的目标线程 ID。
    • 取消前提:取消前必须确保目标线程已经启动运行。
    • 返回值:被取消的线程如果被 pthread_join 等待,其带回的退出结果为 PTHREAD_CANCELED(本质上是 (void*)-1)。

    2.3 线程分离:pthread_detach

    2.3.1 为什么需要线程分离?

    • 默认状态:线程默认是可等待的(joinable)。主线程必须对其进行 pthread_join 释放资源,否则会导致资源泄漏。
    • 分离状态:如果主线程不关心新线程的返回值,只是希望新线程退出后能自动释放自身资源,则应将线程设置为分离状态(detached)。
    • 资源归属:分离后的线程依旧在进程的地址空间中,共享进程的所有资源,只是不再需要也不再允许被主线程 join。

    2.3.2 pthread_detach 接口

    函数原型:

    #include <pthread.h>
    int pthread_detach(pthread_t thread);

    • 可以由主线程调用 pthread_detach(tid) 分离新线程,也可以由新线程在内部调用 pthread_detach(pthread_self()) 分离自己。
    • 注意:一个被分离的线程如果再次被调用 pthread_join,pthread_join 会直接报错返回。

    2.4 Tips:一些值得思考的细节问题

    2.4.1 新线程能不能开更小的线程?

    可以。 在 POSIX 线程模型中,所有线程(包括主线程和新创建的子线程)在权限和地位上都是平等的。任何线程只要具备执行条件,都可以通过调用 pthread_create 来创建新的线程。

    2.4.2 新线程能不能把主线程取消掉?

    在技术接口层面可以,但逻辑上极不推荐。

    • 技术可行性:任何线程只要获取到了主线程的线程 ID(可以通过参数将主线程的 pthread_self() 传给子线程),都可以对主线程调用 pthread_cancel。
    • 实际影响:主线程被取消后,主线程会退出运行,但只要进程中还有其他未分离或未退出的子线程在运行,整个进程就不会立即终止;直到所有线程都退出,进程才会真正终止。这种做法极易引发资源回收逻辑混乱,破坏程序设计的正常生命周期管理。

    2.5 线程终止与线程分离demo

    2.5.1线程分离demo

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>
    #include <cstring>

    void* thread_detach_demo(void* arg) {
    pthread_detach(pthread_self()); // 自我分离
    std::cout << "detached thread running…" << std::endl;
    sleep(1);
    return nullptr;
    }

    int main() {
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_detach_demo, nullptr);

    sleep(2); // 确保子线程运行完成

    // 尝试 join 已分离的线程
    int ret = pthread_join(tid, nullptr);
    if (ret != 0) {
    std::cout << "pthread_join failed: " << strerror(ret) << std::endl;
    }

    return 0;
    }

    2.5.2线程终止demo

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>
    #include <cstring>

    void* thread_exit_demo(void* arg) {
    std::cout << "new thread running…" << std::endl;
    sleep(1);
    pthread_exit((void*)200); // 终止自身并返回 200
    }

    int main() {
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_exit_demo, nullptr);

    void* ret = nullptr;
    pthread_join(tid, &ret);
    std::cout << "thread exit code: " << (long long)ret << std::endl;

    return 0;
    }


    好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye! Linux、C++、算法持续连载中,欢迎关注WeChat Official Account 【此方的技术栈】。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Re:Linux系统篇(五十一)线程篇 · 四:深入pthread库:创建、等待、取消、分离一篇讲透
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!