线程概念与控制(下)

线程概念与控制(中)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(){// 如果线程已经在运行,则返回 falseif (_isrunning)return false;// 创建线程,将当前对象的地址传递给线程入口函数int n = pthread_create(&_tid, nullptr, Routine, this);// 如果创建失败,打印错误信息并返回 falseif (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(){// 如果线程不在运行,则返回 falseif (!_isrunning)return false;// 发送取消请求给线程int n = pthread_cancel(_tid);// 如果取消失败,打印错误信息并返回 falseif (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(){// 如果线程已经被分离,则不能调用 joinif (_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;       // 线程 IDstd::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) // 如果线程已经在运行,则返回 falsereturn 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) // 如果线程不在运行,则返回 falsereturn 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;       // 线程 IDstd::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修饰的全局变量) 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/74112.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL注入之盲注技术详解

SQL注入之盲注技术详解 一、盲注基本概念盲注特点&#xff1a; 二、盲注主要类型1. 布尔盲注判断依据&#xff1a; 2. 时间盲注判断依据&#xff1a; 三、布尔盲注详细技术1. 识别布尔盲注2. 数据提取技术(1) 判断数据库类型(2) 获取数据库名长度(3) 逐字符获取数据库名(4) 获取…

OpenCV 图形API(3)高层次设计概览

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 描述 G-API 是一个异构框架&#xff0c;提供了统一的 API 来使用多个支持的后端编程图像处理流水线。 关键的设计理念是在指定使用哪些内核和设备时保持流…

阿里云Tair KVCache:打造以缓存为中心的大模型Token超级工厂

一、Tair KVCache 简介 Tair KVCache 是阿里云瑶池旗下云数据库 Tair 面向大语言模型推理场景推出的 KVCache 缓存加速服务。 随着互联网技术的演进与流量规模的激增&#xff0c;缓存技术逐渐成为系统架构的核心组件。该阶段催生了 Redis 等开源缓存数据库&#xff0c;阿里巴巴…

Open GL ES ->GLSurfaceView正交投影与透视投影方法中近远平面取值参考

坐标系 OpenGL ES使用右手坐标系&#xff0c;相机默认朝向负z方向 相机位置|vz轴<----- 0 -----> -near -----> -far -----不可见 可见区域 不可见裁剪规则 只有z值在[-near, -far]范围内的物体可见&#xff0c; 当z > -near&#xff08;在近平面前&#…

iOS自定义collection view的page size(width/height)分页效果

前言 想必大家工作中或多或少会遇到下图样式的UI需求吧 像这种cell长度不固定&#xff0c;并且还能实现的分页效果UI还是很常见的 实现 我们这里实现主要采用collection view&#xff0c;实现的方式是自定义一个UICollectionViewFlowLayout的子类&#xff0c;在这个类里对…

Java高频面试之并发编程-01

hello啊&#xff0c;各位观众姥爷们&#xff01;&#xff01;&#xff01;本baby今天来报道了&#xff01;哈哈哈哈哈嗝&#x1f436; 面试官&#xff1a;并行跟并发有什么区别&#xff1f; 并发 vs 并行&#xff1a;核心区别与场景 1. 定义对比 维度并发&#xff08;Concu…

从零开始学Rust:所有权(Ownership)机制精要

文章目录 第四章&#xff1a;Ownership 所有权核心概念关键机制引用与借用&#xff08;Reference & Borrowing&#xff09;悬垂引用问题错误示例分析解决方案引用安全规则 切片&#xff08;Slice&#xff09;内存安全保证 第四章&#xff1a;Ownership 所有权 Ownership i…

一旦懂得,有趣得紧1:词根tempt-(尝试)的两种解法

词根tempt-尝试 tempt vt.引诱&#xff1b;诱惑&#xff1b;怂恿&#xff1b;利诱&#xff1b;劝诱&#xff1b;鼓动 temptation n.引诱&#xff1b;诱惑 // tempt v.引诱 -ation 名词后缀 attempt v.&n.尝试&#xff0c;试图 // at- 加强 tempt 尝试contempt n.蔑视&am…

召唤数学精灵

1.召唤数学精灵 - 蓝桥云课 问题描述 数学家们发现了两种用于召唤强大的数学精灵的仪式&#xff0c;这两种仪式分别被称为累加法仪式 A(n) 和累乘法仪式 B(n)。 累加法仪式 A(n) 是将从1到 n 的所有数字进行累加求和&#xff0c;即&#xff1a; A(n)12⋯n 累乘法仪式 B(n) …

C语言实现查表8位SAE J1850 CRC

背景&#xff1a; 在做霍尔采集电流的时候&#xff0c;CSSV1500N 系列电流传感器通过can数据输出的报文需要做crc校验&#xff0c;嵌入式常用查表的方式&#xff0c;所以就问了下deepseek怎么算这个CRC. 以下是使用 查表法&#xff08;Lookup Table&#xff09; 在C语言中高效…

【UE5.3.2】初学1:适合初学者的入门路线图和建议

3D人物的动作制作 大神分析:3D人物的动作制作通常可以分为以下几个步骤: 角色绑定(Rigging):将3D人物模型绑定到一个骨骼结构上,使得模型能够进行动画控制。 动画制作(Animation):通过控制骨骼结构,制作出人物的各种动作,例如走路、跳跃、打斗等。 动画编辑(Ani…

mapreduce的工作原理

MapReduce 是 Hadoop 中实现分布式并行计算的核心框架&#xff0c;其工作原理基于“分而治之”的思想&#xff0c;将大规模数据处理任务分解为 Map&#xff08;映射&#xff09; 和 Reduce&#xff08;归约&#xff09; 两个阶段。 一、MapReduce 核心流程 1. Input 阶段 - 输…

换季推广不好做?DeepBI用一键托管的方式,让广告投放跑得快、准、稳

每年换季&#xff0c;尤其是春夏、秋冬交替的节点&#xff0c;都是电商平台上各类季节性商品扎堆上新的高峰期。无论是服饰鞋包、家居户外&#xff0c;还是母婴用品、美妆护肤&#xff0c;许多商品都有着强烈的“时间窗口效应”——一旦错过了热卖期&#xff0c;流量下滑迅速&a…

Qt5.14.2+Cmake使用mingw64位编译opencv4.5成功图文教程

​ 一、下载安装相关编译环境软件 1.1 Python3.8&#xff1a;安装路径:C:\Users\Administrator\AppData\Local\Programs\Python\Python38-32 安装包&#xff1a;python3.8.exe 1.2 QT5.14.2&#xff1a;安装路径:C:\Qt\Qt5.14.2 1.3 opencv4.5&#xff1a;解压路径D:\o…

OpenBMC:BmcWeb 处理http请求3 字典树查找节点

OpenBMC:BmcWeb 处理http请求2 查找路由对象-CSDN博客 findRouteByPerMethod实际上是调用了perMethod.trie.find(url);来查找路由对象的 class Trie {struct FindResult{unsigned ruleIndex;std::vector<std::string> params;};FindResult findHelper(const std::string…

Openssl自签证书相关知识

1.前提 检查是否已安装 openssl $ which openssl /usr/bin/openssl 2.建立CA授权中心 2.1.生成ca私钥(ca-prikey.pem) 初始化 OpenSSL 证书颁发机构(CA)的序列号文件 在生成证书时,ca.srl 的初始序列号需正确初始化(如 01),否则可能导致证书冲突 这会将 01 显示在屏幕…

K个一组翻转链表--囊括半数链表题的思想

K 个一组翻转链表 这道算法题就是链表多个算法思想的结合&#xff0c;解决这一道leetcodehot100的链表题至少能做一半了 大概有一下几个点 1.链表定位 2.链表翻转 3.哨兵节点 4.链表合并 看看题目 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff…

Flutter敏感词过滤实战:基于AC自动机的高效解决方案

Flutter敏感词过滤实战&#xff1a;基于AC自动机的高效解决方案 在社交、直播、论坛等UGC场景中&#xff0c;敏感词过滤是保障平台安全的关键防线。本文将深入解析基于AC自动机的Flutter敏感词过滤实现方案&#xff0c;通过原理剖析实战代码性能对比&#xff0c;带你打造毫秒级…

UML中的用例图和类图

在UML&#xff08;统一建模语言&#xff09;中&#xff0c;**用例图&#xff08;Use Case Diagram&#xff09;和类图&#xff08;Class Diagram&#xff09;**是两种最常用的图表类型&#xff0c;分别用于描述系统的高层功能和静态结构。以下是它们的核心概念、用途及区别&…

深入解析:HarmonyOS Design设计语言的核心理念

深入解析&#xff1a;HarmonyOS Design设计语言的核心理念 在当今数字化迅速发展的时代&#xff0c;用户对操作系统的体验要求越来越高。华为的HarmonyOS&#xff08;鸿蒙操作系统&#xff09;应运而生&#xff0c;旨在为用户提供全场景、全设备的智慧体验。其背后的设计语言—…