线程概念与控制(下)

线程概念与控制(中)https://blog.csdn.net/Small_entreprene/article/details/146539064?sharetype=blogdetail&sharerId=146539064&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link对于之前学习的内容,我们现在知道了:

  • Linux没有真正的线程,他是用轻量级进程模拟的,目前我们只停留在见到了(LWP)
  • Linux操作系统提供的接口中,没有直接提供线程的相关接口
  • 作为用户,Linux和用户还有一道鸿沟,所以需要在用户层,需要封装轻量级进程,形成原生线程库-pthread!(用户级别的库)(我们的可执行程序加载,形成进程,动态链接和动态地址重定位,并且要将动态库,加载到内存,然后映射到当前进程的地址空间中!!!)

线程ID及其进程地址空间布局

在 Linux 系统中,用户程序与内核之间存在一层抽象,用户程序需要通过用户级别的库来与内核交互。为了方便用户程序创建和管理线程,Linux 提供了 pthread(POSIX 线程)库,这是一个轻量级的线程库,封装了底层的线程管理机制。当用户程序加载并运行时,它会通过动态链接和动态地址重定位将 pthread 库加载到内存中,并将其映射到当前进程的地址空间,从而允许程序创建和管理多个线程,实现并发执行。

这张图描绘了Linux系统中使用pthread库创建线程时的内存布局和动态链接过程:首先,pthread.so库文件从磁盘加载到物理内存中,并通过动态链接映射到进程的地址空间,包括代码区、数据段和堆区;进程利用mmap系统调用分配动态映射区域或共享区,用于线程间数据共享;每个线程在内核中有一个task_struct结构体来跟踪其状态;线程各自拥有独立的栈空间;用户通过pthread_create()pthread_join()等函数在用户空间中管理线程,这些函数通过系统调用与内核交互,实现线程的创建和同步。

所以,通过pthread_create()pthread_join()等函数,进程自己的代码和数据就可以访问到pthread库内部的代码或者数据了!!!

线程的概念是在库中维护的,因为系统不提供,但是用户需要,所以在库内部(库也是软件),就可能存在多个线程,每个线程有不同的状态,这就需要被管理起来---先描述再组织!!!

在Linux系统中,线程的概念并不是直接由系统内核提供的,而是通过用户空间的库来实现的,其中pthread库就是封装了线程管理功能的原生线程库。这个库内部维护了线程的属性和状态,包括每个线程的不同状态,这些信息被组织和管理起来以支持多线程操作。具体来说,Linux中没有真正意义上的线程,而是使用轻量级进程(LWP)模拟实现线程概念。每个线程在pthread库中都有一个对应的属性结构体,即线程控制块(Thread Control Block,TCB),用于存储线程的状态信息、调度信息、栈信息等。这个结构体在Linux下通常被称为struct pthread,它包含了线程的所有必要信息,以便库可以有效地管理线程。 

在Linux系统中,线程控制块(Thread Control Block,TCB)是用于存储用户线程所有信息的数据结构。TCB的体量比进程控制块(PCB)小非常多,它包含了线程的状态信息、线程的调度信息、线程的栈信息等。具体来说,TCB中通常包含以下信息:

  1. 线程标识符:为每个线程赋予一个唯一的线程标识符。

  2. 一组寄存器:包括程序计数器PC、状态寄存器和通用寄存器的内容。

  3. 线程运行状态:用于描述线程正处于何种运行状态。

  4. 优先级:描述线程执行的优先程度。

  5. 线程专有存储区:用于线程切换时存放现场保护信息,和与该线程相关的统计信息等。

  6. 信号屏蔽:即对某些信号加以屏蔽。

  7. 堆栈指针:在线程运行时,经常会进行过程调用,而过程的调用通常会出现多重嵌套的情况,这样,就必须将每次过程调用中所使用的局部变量以及返回地址保存起来。为此,应为每个线程设置一个堆栈,用它来保存局部变量和返回地址。相应地,在TCB中,也须设置两个指向堆栈的指针:指向用户自已堆栈的指针和指向核心栈的指针。

Linux的线程TCB(或模拟的TCB)包含了以下关键信息:线程ID:唯一标识线程的标识符,确保每个线程都可以被唯一识别。在pthread库中,TCB通常被表示为struct pthread,它包含了线程的状态信息、线程的调度信息、线程的栈信息等。

我怎么能过够在库里面创建一个TCB呢?如果理解不了,我们可以想象一下,我们之前学习C语言,包括文件系统,文件描述符的时候,我们fopen的时候会给我们返回一个FILE*的对象:

FILE *fp = fopen();

其实fopen内部会为我们malloc对应的FILE对象,然后再将地址返回。我们之前不光将其封装了,还将其打包成了库,然后让别人使用,所以,为什么我们能够创建这个TCB,根本原因是我们会调用pthread_create(),其内部就会在系统当中申请相应的TCB,如同fopen也是C标准库,在库里面为我们申请struct file对象。 

上面TCB的内容没有写时间片,上下文...这些与调度有关的是写在内核当中的LWP,也就是PCB中。所以线程的概念:一部分在内核中实现,一部分在用户层来实现。

优点难懂,我们再来看一张图:

上面的内容无非就是阐述说:在我们自己的代码区里,我们调用了 pthread_create()pthread_join()等函数,会动态的让我们在动态库里面,为我们创建一个TCB,TCB描述了线程的相关属性。

那如果库中创建了10个TCB,我们应该如何进行组织呢?

我们看图,其实是我们创建了一个线程之后,那么在库内部就创建了一个:(标红区域:一个管理块:重点由三部分构成:线程TCB,线程局部存储,线程栈)

当我们创建第二个线程的时候,会在动态库中,依据上一个紧挨着的申请一段空间(真实情况不一定挨着),我们管理多线程的数据结构视为数组,其中我们之前代码测试出的pthread_create的返回值,那么大的数字,其实是线程在库当中的,对应的管理块的虚拟地址!!! (函数的返回值就是该管理块的起始地址!!!)

这时候,我们来谈谈为什么线程需要join:

在struct pthread中有一个成员变量void *ret,当对应线程执行完之后,return (void*)10;的时候,其实就会将返回值写到当前该线程的struct pthread中的成员变量void *ret里,所以该线程运行结束了,但是运行结束之后,对应得管理块并没有被释放,所以主线程需要join,因为线程结束时,只是它执行函数完了,但是在库里面,线程控制块并没有结束!!!

并且我们join的时候必须传入对用的tid,找到对应的TCB,拷贝出结构体当中的ret的内容:

join后再将其释放,解决内存泄漏问题!!! (也是我们为什么要使用二级指针的原因:返回值是拷贝出来的)

还有:

创建一个线程就会有对应的线程栈,所以每一个线程,必须有自己独立的栈空间,所对应的栈空间是在自己的pthread库内部,在自己申请的管理块当中,这个栈也有自己的起始虚拟地址,所以主线程用进程地址空间的栈,而创建出来的新线程是使用自己对应的线程栈,所以没有每一个线程都要有自己独立的栈结构;



那么,用户线程和LWP是如何进行联动的呢? 

用户代码调用: pthread_create()

1. 线程控制块的创建

当调用 pthread_create() 时,线程库(如 NPTL)会在用户空间中为新线程分配一个线程控制块(struct pthread)。这个结构体包含了线程的各种属性,例如线程ID、线程栈指针、线程局部存储等。线程控制块存储在进程的共享区中,所有线程都可以访问这个区域。

2. 内核中的轻量级进程(LWP)的创建

在底层,pthread_create() 会通过系统调用 clone() 来创建一个轻量级进程(LWP)。clone() 是 Linux 提供的一个系统调用,用于创建轻量级进程,它允许进程共享资源。pthread_create() 实际上是 clone() 的一个封装。

线程的栈空间是线程运行时用于存储局部变量、函数调用的返回地址等信息的内存区域。在 pthread_create() 中,线程的栈空间通常是在用户空间中分配的。对于主线程,其栈空间是进程地址空间中原生的栈;而对于其他线程,栈空间是在共享区中分配的。

在调用 clone() 时,需要指定子进程(线程)的栈地址。这个栈地址通常是指向分配的栈空间的顶部。例如:

char *stack = malloc(STACK_SIZE);
pid_t pid = clone(child_func, stack + STACK_SIZE, SIGCHLD, NULL);

这里,stack + STACK_SIZE 指向栈的顶部。

这时候我们的内核数据和用户数据就在一定层度上联动起来了!!!(调用pthread_create,既在库中创建线程控制的管理块,又在内核中,调用clone来创建轻量级进程!)

用户线程和LWP的联动主要体现在线程的创建、调度和销毁过程中。当调用pthread_create()时,线程库在用户空间创建线程控制块,并通过clone()系统调用在内核中创建一个LWP,将用户线程与LWP关联起来。线程运行时,线程库在用户空间负责线程的切换,而内核通过LWP的调度管理线程的执行。当线程阻塞或就绪时,线程库会通知内核更新LWP的状态,内核根据这些状态调整调度策略。销毁线程时,线程库清理用户空间资源,并通过系统调用通知内核销毁对应的LWP。这种联动机制使得线程能够在用户空间高效切换,同时利用内核的资源管理功能,确保线程的高效运行和资源的合理分配。(代购:用户层是派发购买任务的,LWP内核层是去完成这个任务的,这么完成的,用户层不关心,只需要完成了,将返回结果带回给struct pthread就可以了

在Linux操作系统中,Linux用户及线程 : 内核LWP = 1 : 1的,对于其他OS,可能是1 : n的。

现在我们就可以粗力度的解决下列问题:

  1. 线程ID:是我们pthread_create的时候,我们在库当中创建的描述线程的线程控制块的起始虚拟地址,所以导致地址非常大;
  2. 线程返回值:是线程执行完,将该线程的退出结果写到线程控制块的对应的结构体的void* ret的内容当中,然后通过join得到;
  3. 线程分离:在线程控制块(TCB)中,有一个线程状态,默认int joinable = 1,表明这个线程不分离,0的时候是分离的,线程一旦在底层退出了,识别到上层控制块对应结构体里面joinable的字段为0,那么该线程就自动释放。(joinable本质就是一个标志位)

因为动态库是共享的,可以被映射到对应进程的虚拟地址空间上,所以Linux所有线程,都在库中:

只不过互相访问不了,因为每一个线程只能拿到自己线程控制块的虚拟地址。

这里创建线程要申请的线程控制块不是通过malloc出来的,是通过mmap机制申请出来的,其实mmap就是共享内存,只不过我们执勤学习的共享内存是System V标准,mmap是POSIX标准的:用于将文件或设备映射到进程的地址空间。它是一种内存映射文件 I/O 的方法,允许进程像操作内存一样直接访问文件内容。

也就是mmap是物理地址空间的一段共享内存,可以映射到不同进程的虚拟地址空间上,如果内容在磁盘上,就可以将文件内容映射到物理共享内存,不同进程就可以访问该共享内存,达到不需要文件描述符,就可以访问磁盘的文件。所以mmap可以实现线程申请空间,进程间通信,文件映射。

线程栈

通过上面的学习,我们知道了每一个线程都有自己独立的栈结构了:

每一个线程都有:

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

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

虽然 Linux 将线程和进程不加区分的统⼀到了 task_struct ,但是对待其地址空间的 stack 还是有些区别的。

  • 对于 Linux 进程或者说主线程,简单理解就是 main 函数的栈空间,在 fork 的时候,实际上就是复制了⽗亲的 stack 空间地址,然后写时拷贝(cow)以及动态增⻓。如果扩充超出该上限则栈溢出会报段错误(发送段错误信号给该进程)。进程栈是唯⼀可以访问未映射⻚⽽不⼀定会发⽣段错误的 —— 超出扩充上限才报。
  • 然⽽对于主线程⽣成的⼦线程⽽⾔,其 stack 将不再是向下⽣⻓的,⽽是事先固定下来的。线程栈⼀般是调⽤ glibc/uclibc 等的 pthread 库接⼝ pthread_create 创建的线程,在⽂件映射区(或称之为共享区)。其中使⽤ mmap 系统调⽤,这个可以从 glibc 的 nptl/allocatestack.c 中的 allocate_stack 函数中看到:
mem = mmap (NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

此调⽤中的 size 参数的获取很是复杂,你可以⼿⼯传⼊ stack 的⼤⼩,也可以使⽤默认的,⼀般⽽⾔就是默认的 8M 。这些都不重要,重要的是,这种 stack 不能动态增⻓,⼀旦⽤尽就没了,这是和⽣成进程的 fork 不同的地⽅。在 glibc 中通过 mmap 得到了 stack 之后,底层将调⽤ sys_clone 系统调⽤:

int sys_clone(struct pt_regs *regs)
{
    unsigned long clone_flags;
    unsigned long newsp;
    int __user *parent_tidptr, *child_tidptr;
    clone_flags = regs->bx;
    // 获取了 mmap 得到的线程的 stack 指针
    newsp = regs->cx;
    parent_tidptr = (int __user *)regs->dx;
    child_tidptr = (int __user *)regs->di;
    if (!newsp)
        newsp = regs->sp;
    return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}

因此,对于⼦线程的 stack ,它其实是在进程的地址空间中 map 出来的⼀块内存区域,原则上是线程私有的(对应线程控制块才能过找到:其实都能是“共享的”,只是能不能找到对应的虚拟地址),但是同⼀个进程的所有线程⽣成的时候,是会浅拷⻉⽣成者的 task_struct 的很多字段,如果愿意,其它线程也还是可以访问到的,于是⼀定要注意。

线程封装

有了上面的只是理论储备,接下来,我们来封装一下线程,以便更好的认识线程:

源代码第一版

#ifndef _THREAD_H_
#define _THREAD_H_

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

namespace ThreadModlue
{
    // 用于生成线程名称的序号
    // 注意:此变量是静态的,仅在当前文件内可见。如果此头文件被多个源文件包含,
    // 可能会导致每个文件中都有一个独立的 `number`,从而导致线程名称重复。
    static uint32_t number = 1;

    class Thread
    {
        // 使用 std::function 封装线程的回调函数,支持函数指针、lambda 表达式等
        using func_t = std::function<void()>;

    private:
        // 设置线程为分离状态
        void EnableDetach()
        {
            std::cout << "线程被分离了" << std::endl;
            _isdetach = true;
        }

        // 设置线程为运行状态
        void EnableRunning()
        {
            _isrunning = true;
        }

        // 线程的入口函数,必须是静态函数或全局函数,因为 pthread_create 要求入口函数为 C 风格
        static void *Routine(void *args)
        {
            // 将传入的 void* 转换为 Thread 类型的指针
            Thread *self = static_cast<Thread *>(args);

            // 设置线程为运行状态
            self->EnableRunning();

            // 如果线程被分离,则调用 Detach 方法
            if (self->_isdetach)
                self->Detach();

            // 设置线程名称,注意:pthread_setname_np 是非标准扩展,可能在某些平台上不可用
            pthread_setname_np(self->_tid, self->_name.c_str());

            // 调用用户提供的回调函数
            self->_func();

            // 返回 nullptr 表示线程正常结束
            return nullptr;
        }

    public:
        // 构造函数,初始化线程对象
        Thread(func_t func)
            : _tid(0), // 线程 ID 初始化为 0
              _isdetach(false), // 线程默认不是分离状态
              _isrunning(false), // 线程默认未运行
              res(nullptr), // 线程返回值初始化为 nullptr
              _func(func) // 用户提供的回调函数
        {
            // 生成线程名称,格式为 "thread-序号"
            _name = "thread-" + std::to_string(number++);
        }

        // 分离线程
        void Detach()
        {
            // 如果线程已经被分离,则直接返回
            if (_isdetach)
                return;

            // 如果线程正在运行,则调用 pthread_detach 将线程分离
            if (_isrunning)
                pthread_detach(_tid);

            // 设置线程为分离状态
            EnableDetach();
        }

        // 启动线程
        bool Start()
        {
            // 如果线程已经在运行,则返回 false
            if (_isrunning)
                return false;

            // 创建线程,将当前对象的地址传递给线程入口函数
            int n = pthread_create(&_tid, nullptr, Routine, this);

            // 如果创建失败,打印错误信息并返回 false
            if (n != 0)
            {
                std::cerr << "create thread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                // 打印线程创建成功的信息
                std::cout << _name << " create success" << std::endl;
                return true;
            }
        }

        // 停止线程
        bool Stop()
        {
            // 如果线程不在运行,则返回 false
            if (!_isrunning)
                return false;

            // 发送取消请求给线程
            int n = pthread_cancel(_tid);

            // 如果取消失败,打印错误信息并返回 false
            if (n != 0)
            {
                std::cerr << "stop thread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                // 设置线程为非运行状态
                _isrunning = false;

                // 打印线程停止的信息
                std::cout << _name << " stop" << std::endl;
                return true;
            }
        }

        // 等待线程结束
        void Join()
        {
            // 如果线程已经被分离,则不能调用 join
            if (_isdetach)
            {
                std::cout << "你的线程已经是分离的了,不能进行join" << std::endl;
                return;
            }

            // 等待线程结束,并获取返回值
            int n = pthread_join(_tid, &res);

            // 如果 join 失败,打印错误信息
            if (n != 0)
            {
                std::cerr << "join thread error: " << strerror(n) << std::endl;
            }
            else
            {
                // 打印 join 成功的信息
                std::cout << "join success" << std::endl;
            }
        }

        // 析构函数
        ~Thread()
        {
            // 注意:当前析构函数为空,没有处理线程的销毁。
            // 如果线程仍在运行,应该等待线程结束或者分离线程,否则可能导致未定义行为。
        }

    private:
        pthread_t _tid;       // 线程 ID
        std::string _name;    // 线程名称
        bool _isdetach;       // 是否分离
        bool _isrunning;      // 是否运行
        void *res;            // 线程返回值
        func_t _func;         // 用户提供的回调函数
    };
}

#endif

源代码详细解释

这段代码实现了一个简单的线程类封装,基于 POSIX 线程库(pthread)。它提供了一个方便的接口来创建、启动、停止、分离和等待线程,并且支持线程名称的设置和线程回调函数的封装。以下是对代码的详细解释:

1. 头文件保护

#ifndef _THREAD_H_
#define _THREAD_H_

这是标准的头文件保护宏,用于防止头文件被重复包含。如果 _THREAD_H_ 已经被定义,则不会再次包含该头文件,从而避免重复定义的问题。

2. 包含的头文件

#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>
  • <iostream><string> 提供了输入输出流和字符串操作的功能。

  • <pthread.h> 是 POSIX 线程库的头文件,提供了线程创建、管理等函数。

  • <cstdio><cstring> 提供了标准输入输出和字符串操作的 C 风格函数。

  • <functional> 提供了 std::function,用于封装可调用对象(如函数指针、lambda 表达式等)。

3. 命名空间和静态变量

namespace ThreadModlue
{
    static uint32_t number = 1;
  • ThreadModlue 是一个命名空间,用于封装线程相关的类和变量,避免命名冲突。

  • number 是一个静态变量,用于生成线程名称的序号。它在当前文件内可见,每次创建线程时递增,用于生成唯一的线程名称。

4. 线程类的定义

class Thread
{
    using func_t = std::function<void()>;
  • Thread 类封装了线程的创建、管理等功能。

  • func_t 是一个类型别名,表示线程的回调函数类型,使用 std::function<void()>,可以接受函数指针、lambda 表达式等可调用对象。

5. 私有成员函数

void EnableDetach()
{
    std::cout << "线程被分离了" << std::endl;
    _isdetach = true;
}

void EnableRunning()
{
    _isrunning = true;
}
  • EnableDetach:将线程标记为分离状态,并打印提示信息。

  • EnableRunning:将线程标记为运行状态。

6. 静态入口函数

static void *Routine(void *args)
{
    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;
}
  • Routine 是线程的入口函数,必须是静态函数或全局函数,因为 pthread_create 要求入口函数为 C 风格。

  • 它接收一个 void* 参数,将其转换为 Thread 类型的指针。

  • 设置线程为运行状态,如果线程被分离,则调用 Detach 方法。

  • 使用 pthread_setname_np 设置线程名称(注意:这是非标准扩展,可能在某些平台上不可用)。

  • 调用用户提供的回调函数 _func

  • 返回 nullptr 表示线程正常结束。

7. 公有成员函数

Thread(func_t func)
    : _tid(0), _isdetach(false), _isrunning(false), res(nullptr), _func(func)
{
    _name = "thread-" + std::to_string(number++);
}
  • 构造函数初始化线程对象,设置线程 ID、分离状态、运行状态、返回值和用户提供的回调函数。

  • 生成线程名称,格式为 "thread-序号"number 递增。

void Detach()
{
    if (_isdetach)
        return;
    if (_isrunning)
        pthread_detach(_tid);
    EnableDetach();
}
  • Detach 方法将线程分离。如果线程已经在分离状态,则直接返回;如果线程正在运行,则调用 pthread_detach 将线程分离。

bool Start()
{
    if (_isrunning)
        return false;
    int n = pthread_create(&_tid, nullptr, Routine, this);
    if (n != 0)
    {
        std::cerr << "create thread error: " << strerror(n) << std::endl;
        return false;
    }
    else
    {
        std::cout << _name << " create success" << std::endl;
        return true;
    }
}
  • Start 方法启动线程。如果线程已经在运行,则返回 false

  • 使用 pthread_create 创建线程,将当前对象的地址传递给线程入口函数。

  • 如果创建失败,打印错误信息并返回 false;否则打印线程创建成功的信息并返回 true

bool Stop()
{
    if (!_isrunning)
        return false;
    int n = pthread_cancel(_tid);
    if (n != 0)
    {
        std::cerr << "stop thread error: " << strerror(n) << std::endl;
        return false;
    }
    else
    {
        _isrunning = false;
        std::cout << _name << " stop" << std::endl;
        return true;
    }
}
  • Stop 方法停止线程。如果线程不在运行,则返回 false

  • 使用 pthread_cancel 发送取消请求给线程。

  • 如果取消失败,打印错误信息并返回 false;否则设置线程为非运行状态并返回 true

void Join()
{
    if (_isdetach)
    {
        std::cout << "你的线程已经是分离的了,不能进行join" << std::endl;
        return;
    }
    int n = pthread_join(_tid, &res);
    if (n != 0)
    {
        std::cerr << "join thread error: " << strerror(n) << std::endl;
    }
    else
    {
        std::cout << "join success" << std::endl;
    }
}
  • Join 方法等待线程结束。如果线程已经被分离,则不能调用 join

  • 使用 pthread_join 等待线程结束,并获取返回值。

  • 如果 join 失败,打印错误信息;否则打印成功信息。

8. 私有成员变量

pthread_t _tid;
std::string _name;
bool _isdetach;
bool _isrunning;
void *res;
func_t _func;
  • _tid:线程 ID。

  • _name:线程名称。

  • _isdetach:是否分离。

  • _isrunning:是否运行。

  • res:线程返回值。

  • _func:用户提供的回调函数。

9. 析构函数

~Thread()
{
    // 注意:当前析构函数为空,没有处理线程的销毁。
    // 如果线程仍在运行,应该等待线程结束或者分离线程,否则可能导致未定义行为。
}
  • 析构函数目前为空,没有处理线程的销毁。

  • 如果线程仍在运行,应该等待线程结束或者分离线程,否则可能导致未定义行为。

这段代码实现了一个简单的线程类封装,提供了线程创建、启动、停止、分离和等待的功能,并支持线程名称的设置和线程回调函数的封装。它基于 POSIX 线程库,使用了 C++ 的 std::function 来支持多种类型的回调函数。

源代码第二版-tmplate模板化

在之前的代码中,我们实现了一个简单的线程类封装,支持线程的创建、启动、停止、分离和等待功能。然而,之前的实现存在一些局限性,例如线程回调函数只能是无参的 std::function<void()>,这限制了线程任务的灵活性。此外,线程名称的序号变量 number 是静态的,可能会导致线程名称重复的问题。

为了进一步提升线程类的灵活性和功能,我们对代码进行了改进。改进后的代码支持带参数的线程回调函数,并且通过模板化的方式,允许用户传递不同类型的数据给线程任务。此外,我们还修复了线程名称序号变量的潜在问题,确保线程名称的唯一性。

以下是改进后的代码,包含详细的注释:

#ifndef _THREAD_H_
#define _THREAD_H_

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

namespace ThreadModlue
{
    // 使用静态局部变量来生成线程名称的序号,确保线程名称的唯一性
    // 修复了之前静态变量可能导致的线程名称重复问题
    static uint32_t GetThreadId()
    {
        static uint32_t number = 1; // 静态局部变量,只初始化一次
        return number++;            // 返回当前值并递增
    }

    // 模板类 Thread,支持带参数的线程回调函数
    template <typename T>
    class Thread
    {
        // 定义线程回调函数的类型,支持带参数的函数
        using func_t = std::function<void(T)>;

    private:
        // 设置线程为分离状态
        void EnableDetach()
        {
            std::cout << "线程被分离了" << std::endl;
            _isdetach = true;
        }

        // 设置线程为运行状态
        void EnableRunning()
        {
            _isrunning = true;
        }

        // 线程的入口函数,必须是静态函数或全局函数
        // 修复了之前静态成员函数的潜在问题
        static void *Routine(void *args)
        {
            Thread<T> *self = static_cast<Thread<T> *>(args); // 将 void* 转换为 Thread 类型的指针
            self->EnableRunning();                            // 设置线程为运行状态
            if (self->_isdetach)
                self->Detach();                               // 如果线程被分离,则调用 Detach 方法
            self->_func(self->_data);                         // 调用用户提供的回调函数,并传递数据
            return nullptr;                                   // 返回 nullptr 表示线程正常结束
        }

    public:
        // 构造函数,初始化线程对象
        Thread(func_t func, T data)
            : _tid(0),                // 线程 ID 初始化为 0
              _isdetach(false),       // 线程默认不是分离状态
              _isrunning(false),      // 线程默认未运行
              res(nullptr),           // 线程返回值初始化为 nullptr
              _func(func),            // 用户提供的回调函数
              _data(data)             // 用户传递给线程的数据
        {
            // 生成线程名称,格式为 "thread-序号"
            _name = "thread-" + std::to_string(GetThreadId());
        }

        // 分离线程
        void Detach()
        {
            if (_isdetach) // 如果线程已经被分离,则直接返回
                return;
            if (_isrunning) // 如果线程正在运行,则调用 pthread_detach 将线程分离
                pthread_detach(_tid);
            EnableDetach(); // 设置线程为分离状态
        }

        // 启动线程
        bool Start()
        {
            if (_isrunning) // 如果线程已经在运行,则返回 false
                return false;
            int n = pthread_create(&_tid, nullptr, Routine, this); // 创建线程
            if (n != 0) // 如果创建失败,打印错误信息并返回 false
            {
                std::cerr << "create thread error: " << strerror(n) << std::endl;
                return false;
            }
            else // 打印线程创建成功的信息
            {
                std::cout << _name << " create success" << std::endl;
                return true;
            }
        }

        // 停止线程
        bool Stop()
        {
            if (!_isrunning) // 如果线程不在运行,则返回 false
                return false;
            int n = pthread_cancel(_tid); // 发送取消请求给线程
            if (n != 0) // 如果取消失败,打印错误信息并返回 false
            {
                std::cerr << "stop thread error: " << strerror(n) << std::endl;
                return false;
            }
            else // 设置线程为非运行状态并返回 true
            {
                _isrunning = false;
                std::cout << _name << " stop" << std::endl;
                return true;
            }
        }

        // 等待线程结束
        void Join()
        {
            if (_isdetach) // 如果线程已经被分离,则不能调用 join
            {
                std::cout << "你的线程已经是分离的了,不能进行join" << std::endl;
                return;
            }
            int n = pthread_join(_tid, &res); // 等待线程结束
            if (n != 0) // 如果 join 失败,打印错误信息
            {
                std::cerr << "join thread error: " << strerror(n) << std::endl;
            }
            else // 打印 join 成功的信息
            {
                std::cout << "join success" << std::endl;
            }
        }

        // 析构函数
        ~Thread()
        {
            // 注意:当前析构函数为空,没有处理线程的销毁。
            // 如果线程仍在运行,应该等待线程结束或者分离线程,否则可能导致未定义行为。
        }

    private:
        pthread_t _tid;       // 线程 ID
        std::string _name;    // 线程名称
        bool _isdetach;       // 是否分离
        bool _isrunning;      // 是否运行
        void *res;            // 线程返回值
        func_t _func;         // 用户提供的回调函数
        T _data;              // 用户传递给线程的数据
    };
}

#endif

改进点说明

线程名称序号的改进:使用静态局部变量 GetThreadId 函数来生成线程名称的序号,确保线程名称的唯一性。静态局部变量只在第一次调用时初始化,并且在程序运行期间保持其值,从而避免了之前静态变量可能导致的线程名称重复问题。

支持带参数的线程回调函数:通过模板化的方式,允许用户传递不同类型的数据给线程任务。线程回调函数的类型为 std::function<void(T)>,其中 T 是用户定义的数据类型。这样可以更灵活地处理线程任务。

静态成员函数的修复:静态成员函数 Routine 修复了之前可能存在的问题,确保线程入口函数的正确性。

这些改进使得线程类更加灵活和健壮,能够更好地满足实际开发中的需求。

线程局部存储

每一个线程创建时,他会库里面创建描述线程的结构体struct pthread,内部有指针指向自己对应的线程栈,可是线程局部存储是个什么东西?

我们先来看一个代码:

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

int count = 1;

std::string Addr(int &c)
{
    char addr[64];
    snprintf(addr, sizeof(addr), "%p", &c);
    return addr;
}

void *routine1(void *args)
{
    (void)args;
    while (true)
    {
        std::cout << "thread - 1, count = " << count << "[我来修改count], "
                  << "&count: " << Addr(count) << std::endl;
        count++;
        sleep(1);
    }
}

void *routine2(void *args)
{
    (void)args;
    while (true)
    {
        std::cout << "thread - 2, count = " << count
                  << ", &count: " << Addr(count) << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid1, tid2; // 创建两个线程,分别执行不同的任务
    pthread_create(&tid1, nullptr, routine1, nullptr);
    pthread_create(&tid2, nullptr, routine2, nullptr);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

代码中定义了一个全局变量 count,并创建了两个线程。第一个线程(routine1)不断修改全局变量 count 的值,并打印当前的 count 值和它的地址。第二个线程(routine2)则只读取 count 的值并打印。(由于两个线程同时访问和修改同一个全局变量,而没有采取任何同步措施,因此可能会出现竞争条件,导致输出结果不可预测,甚至可能出现数据错误。所以我们通过sleep来确保看到的现象是OK的,主要是看一个现象,对其保护会在后面谈到)

一个修改一个打印,因为这是共享的资源,所以不会发生写时拷贝。

但是我们给全局共享的变量count前加修饰__thread:

我们发现count前加修饰__thread之后,打印出来的结果说明的是:对应两个的count不再是一个相同的一个地址上的变量了。

所以:我们就称为:变量count前加修饰__thread后,该count叫做线程的局部存储!

其实__thread是一个我们引导编译器的选项,实际上就是我们编译这一份代码的时候,这个修饰后的额count并不会在已初始化数据段上去帮我们定义,他会将其count变量在当前线程的局部存储当中开辟一份,只不过变量名都是count,但是底层的虚拟地址此时就不一样了。

这个现象,就是线程的局部存储!

那么线程局部存储有什么用?

有时候,我们创建线程的时候,我们往往需要有全局变量,比如说。。,但是我又不想让这个全局变量被其他线程看到!所以我们可是利用__thread来实现线程局部存储。

官方点就是:

线程局部存储(Thread Local Storage,TLS)是一种特殊的存储机制,用于为每个线程提供独立的变量副本。即使多个线程访问同一个变量名,它们看到的其实是各自线程中的独立副本,而不是共享的全局变量。线程局部存储的主要用途是解决多线程环境中的数据隔离问题,避免线程之间的数据冲突和竞争条件。

1. 避免竞争条件

在多线程程序中,全局变量通常会被多个线程共享和访问。如果没有适当的同步机制(如互斥锁),可能会导致竞争条件,使得程序的行为不可预测。线程局部存储可以为每个线程提供独立的变量副本,从而避免这种问题。

2. 线程特定数据

有些数据是线程特定的,每个线程需要有自己的独立副本。例如:

  • 每个线程有自己的日志记录器、配置信息或用户上下文。

  • 每个线程有自己的临时存储空间或缓冲区。

使用线程局部存储可以方便地管理这些线程特定的数据,而无需手动为每个线程分配和管理独立的变量。(方便,爽!!!)

3. 性能优化

使用互斥锁等同步机制虽然可以解决竞争条件,但会引入额外的性能开销,尤其是在高并发场景下。线程局部存储可以避免这种开销,因为每个线程访问的是自己的独立副本,无需同步。

我们需要注意的是:

线程局部存储,只能存储内置类型和部分指针(不要说class/lambda/函数,这可不行)


pthread_setname_nppthread_getname_np 是两个用于设置和获取线程名称的函数,它们在 Linux 系统中广泛用于调试和监控多线程程序。

#include <pthread.h>

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

    • 用于为指定线程设置名称。线程名称是一个以空字符结尾的字符串,最大长度为 16 个字符(包括空字符)。如果名称长度超过 16 个字符,函数会返回错误码 ERANGE

    • 参数:

      • thread:要设置名称的线程的标识符。

      • name:指向线程名称的字符串指针。

    • 返回值:

      • 成功时返回 0,失败时返回错误码。

  • pthread_getname_np

    • 用于获取指定线程的名称。名称存储在提供的缓冲区中,缓冲区长度至少应为 16 个字符。

    • 参数:

      • thread:要获取名称的线程的标识符。

      • name:用于存储线程名称的缓冲区。

      • len:缓冲区的长度。

    • 返回值:

      • 成功时返回 0,失败时返回错误码。

 

其原理就是我们的线程局部存储:

pthread_setname_nppthread_getname_np 是用于设置和获取线程名称的函数,它们通过操作线程的内部控制结构(如线程控制块 TCB)来实现。线程局部存储(TLS)则是为每个线程提供独立变量副本的机制,确保线程间数据隔离。虽然设置线程名称的操作本身不直接涉及 TLS,但它们都基于线程的内部数据结构来管理线程相关的信息,线程名称和线程局部变量都存储在每个线程的独立空间中,从而实现线程级别的数据隔离和管理。(就是可以看成mame就是一个__pthread修饰的全局变量) 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值