- 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中通常包含以下信息:
线程标识符:为每个线程赋予一个唯一的线程标识符。
一组寄存器:包括程序计数器PC、状态寄存器和通用寄存器的内容。
线程运行状态:用于描述线程正处于何种运行状态。
优先级:描述线程执行的优先程度。
线程专有存储区:用于线程切换时存放现场保护信息,和与该线程相关的统计信息等。
信号屏蔽:即对某些信号加以屏蔽。
堆栈指针:在线程运行时,经常会进行过程调用,而过程的调用通常会出现多重嵌套的情况,这样,就必须将每次过程调用中所使用的局部变量以及返回地址保存起来。为此,应为每个线程设置一个堆栈,用它来保存局部变量和返回地址。相应地,在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的。
现在我们就可以粗力度的解决下列问题:
- 线程ID:是我们pthread_create的时候,我们在库当中创建的描述线程的线程控制块的起始虚拟地址,所以导致地址非常大;
- 线程返回值:是线程执行完,将该线程的退出结果写到线程控制块的对应的结构体的void* ret的内容当中,然后通过join得到;
- 线程分离:在线程控制块(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_np
和pthread_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_np
和 pthread_getname_np
是用于设置和获取线程名称的函数,它们通过操作线程的内部控制结构(如线程控制块 TCB)来实现。线程局部存储(TLS)则是为每个线程提供独立变量副本的机制,确保线程间数据隔离。虽然设置线程名称的操作本身不直接涉及 TLS,但它们都基于线程的内部数据结构来管理线程相关的信息,线程名称和线程局部变量都存储在每个线程的独立空间中,从而实现线程级别的数据隔离和管理。(就是可以看成mame就是一个__pthread修饰的全局变量)