简单封装线程库 + 理解LWP和TID

文章目录

前言:

在上一文的线程控制中,我们先是聊了关于为什么我们要在编译链接时将线程库给链接起来,简单回顾一下,其根本原因,就是因为我们理解的线程和Linux中的线程是两个不同的概念,在Linux操作系统中,只认"LWP"即轻量级进程,而为了能够很好的适配我们所理解的线程,线程库pthread就出现了。

然后紧接着我们学习了控制线程的一系列操作,包括线程的创建、线程的等待、线程的终止、线程的分离。

这部分的学习和当年我们学习进程的一系列操作一致。

那我们在这里想要简单回顾一下Linux下,进程和线程之间的关系:

特性 进程 线程
定义 程序的执行实例 进程中的执行单元
内存 每个进程有独立的地址空间 线程共享进程的内存空间
资源 独立的资源(如文件描述符、内存) 共享进程资源,独立的栈空间
调度 每个进程是内核调度的基本单位 线程是进程内部的调度实体
创建开销 较大(需要复制资源) 较小(共享进程资源)
并发/并行 进程可以并行执行,但开销较大 线程可以并发执行,开销小

简单封装一下C++线程库

为了能向后期学习C++11中的线程库看齐,我们在这里以前先自己模拟手搓一个线程库出来,那么说干就干,首先我们先出个简易的步骤:

  1. "先描述",一个简单简易的线程类,应该包含一个线程的名字tid运行状态要执行的回调函数
  2. 构造函数直接打印一条消息即可,在main函数中创建线程使用emplace_back直接构造即可。
  3. 使线程执行操作,直接调用函数指针(这里会存在一个关于this指针的问题,一会聊)。
  4. 终止线程操作
  5. join等待回收线程。
  • 构造函数及私有成员:

    c++ 复制代码
    class My_Thread
    {
        // 构造函数
    	Thread(std::string name, Func_t func)
        	: _name(name)
        	, _func(func)
    	{
    		std::cout << _name << " have been created!" << std::endl;
    	}
    	
        // ... 其它操作
        
    	// 私有成员属性
    	private:
    		std::string _name;   // 名字 ("thread-1 ...")
    		pthread_t _tid;      // tid
    		Func_t _func;        // 回调函数地址
    		bool _isrunning;     // 运行状态
    };
  • 执行线程

    本质还是在这一步使用pthread_create()函数创建线程,然后通过其参数里的"函数指针"实现操作!

    但是!

    因为咱们本质是在类里面调用的pthread_create()函数的,而若我们通过函数指针调用的函数也定义在该类步,那么就会出现报错!

    原因就在于,Threadd_Routine()函数内部,还隐含了一个this指针!

    解决办法也很简单,我们可以在该函数前面++添加static关键字++

    简单回顾一下,当年学习C++时我们说过,添加static关键字修饰的函数,会存放在静态区,属于是其它类也可以用,而不是是独属某个类.

    因此在这里我们可以修改修改:

    c++ 复制代码
    void Execute()
    {
    	_isrunning = true;
    	_func(_name);
    	_isrunning = false;
    }
    
    // 如果不加static修饰的话,该函数就是此类独有的,那这个函数的参数就会隐含一个this指针!
    // 加上static关键字进行修饰,就能保证该函数是存在于静态区的,不是某个类独有的!
    static void* Thread_Routine(void *args)
    {
    	My_Thread *Self = (My_Thread *)args;
    	Self->Execute();   // 在这里当然可以直接调用_func,但是代码健壮性太烂,不雅观
    	return nullptr;
    }
    
    bool Start()
    {
    	// 创建线程
    	int n = ::pthread_create(&_tid, nullptr, Thread_Routine, this); // 因为Thread_Routine不独属于某个类,所以在这里传递"当前类"this.
    	if (n != 0)
        	return false;
    	return true;
    }
  • 其余部分(终止线程,等待回收线程):

    c++ 复制代码
    void Stop()
    {
    	while (!_isrunning)
    	{
    		::pthread_cancel(_tid);
    		_isrunning = false;
    		std::cout << _name << " Stop" << std::endl;
    		break;
    	}
    }
    
    void Join()
    {
    	if(!_isrunning)
    	{
    		int n = ::pthread_join(_tid, nullptr);
    		if (n == 0)
    		{
    			std::cout << _name << " join suceess!" << std::endl;
    		}
    	}
    }
    
    ~My_Thread() {}

main函数编写:

c++ 复制代码
void Print(const std::string &name)
{
    int cnt = 1;
    while (true)
    {
        std::cout << name << " is running... cnt: " << cnt << std::endl;
        cnt++;
        sleep(1);
    }
}

int main()
{
    std::vector<My_Thread> threads;
    for (int i = 1; i <= 5; ++i)
    {
        std::string name = "thread-" + std::to_string(i);
        threads.emplace_back(name, Print);
        sleep(1);
    }

    for (auto &t : threads)
    {
        t.Start();
    }

    sleep(10);

    // 结束多线程
    for (auto &t : threads)
    {
        t.Stop();
    }

    // 等待多线程
    for (auto &t : threads)
    {
        t.Join();
    }

    return 0;
}

运行结果:

如何理解tid?

相信学到这里,你或多或少有对线程的LWP的编号和tid有疑问。那么别担心,这一部分我们会将tid以及LWP彻底讲解清楚!

  • LWP?PCB?

    首先我们先来回顾一下,曾经我说过!------------ "Linux操作系统,在内核角度来看,只认识LWP(轻量级进程)"

    所以其实在Linux内核中,没有我们曾将讲解的PCB,也就是进程控制块,这个是"不存在"的!

    取而代之的,其实是一个一个的LWP,也就是轻量级进程

  • tid?LWP?

    我们在谈"创建线程"时,我们介绍了函数pthread_create(),他有一个参数名字是pthread_t *thread,而如果创建成功了,这个thread就会获得值,这个值就代表的是++线程的ID++。

    可是,咱们上次创建了线程,查看到LWP也有一个值,类比一下进程的pid,是一个进程的标识。

    那你通过pthread_create()创建后得到的thread,它也是线程的ID,那他是tid吗?如果是,那LWP的值又算什么呢?

理解pthread库:

为了能够较好的理解上述的tid和LWP的关系,我们首先需要先来认识理解pthread库。

还是一样,先回顾库是怎么加载的!

有一天你写好了一段关于创建线程的代码,这个代码会存放在磁盘上。

又有一天,你想跑这个代码了,于是你打开代码编译链接形参可执行程序的过程中,经历了这几个过程:

  1. 我将你的代码编译之后拿到++物理内存++中
  2. 在我进行查看链接时,我发现你需要链接libpthread.so这个库,于是我就从磁盘中把这个库也**++拿到物理内存++**中
  3. 接下来为了能让Linux内核也能看到,于是我把它通过页表,映射到虚拟地址空间中。具体的位置就位于堆栈之间的++共享区++

内核视角与用户视角:

基于上面的理解,我们能得出一个结论,那就是Linux操作系统内核是不维护我们认为的线程的!

本质上,Linux操作系统不认可线程,而是只在乎它自己的LWP,这也就是为什么"线程"也被称为++"用户级线程"++。

正因为要适配用户的需求,那我们就得来实现维护和封装。

那么我们又该怎么去维护,或者说管理我们的++用户级线程++的呢? ------ 先描述,再组织!

  • 所以我们使用pthread_create()函数创建的用户级线程的相关属性,就都会存放在了这个动态库里

  • 至此,我们可以总结 ------------ 记录线程的相关属性和操作都会被记录在这"线程控制块"内,其会被存放在动态库里!

  • 而tid是什么? ------ tid就是该线程块的起始地址!

  • 这个可能有点难理解,就想像我们在学习C++的unordered_map时,我们只是使用并不关心其内部的接口是怎么实现,我们也学过那些哈希表上的节点不也是malloc出来的吗,而这些节点其实就是在它的库里进行维护的!

  • 其实新线程执行完后返回,在内部来看,其实LWP是被释放了的,但是库里面的用户级线程还没有被释放,它还记录着返回值信息,如果碰到主线程在join,那就会把返回值信息拷贝给当初的void* ret ,然后自己再释放,这就是为什么当时我会说可能出现"僵尸线程"这种情况

  • 用户级线程通过pthread_create创建,而pthread_create内部又会调用系统调用接口clone来创建系统级线程LWP

  • "线程控制块"里有一个线程栈,这就正好对应上我之前讲解过的,每个线程都有一套独属于自己的栈空间。

    而这个线程栈,当然不是库里面的空间,它可能也是个指针,然后记录了多大的范围,告诉线程和Linux说,在虚拟地址空间中,你线程的栈空间就是这么大。

其实就一句话:
用户级线程其实就是一个在库函数里的结构体!

相关推荐
CodeClimb17 分钟前
【华为OD-E卷 - 服务失效判断 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
CodeClimb19 分钟前
【华为OD-E卷 - 九宫格按键输入 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
14_1122 分钟前
Cherno C++学习笔记 P49 C++中使用静态库
c++·笔记·学习
小仇学长36 分钟前
Linux内核编程(二十一)USB应用及驱动开发
linux·驱动开发·usb
代码背包客39 分钟前
制作一个纯净版的系统镜像(Linux,Window都可以)
linux·运维·服务器·windows
Zevalin爱灰灰43 分钟前
FreeRTOS从入门到精通 第七章(FreeRTOS列表和列表项)
stm32·操作系统·freertos
pumpkin8451443 分钟前
C++移动语义
开发语言·c++
萨克・麦・迪克1 小时前
Unix/Linux 系统中环境变量有哪些
linux·服务器
山兔11 小时前
19.3、Unix Linux安全分析与防护
linux·安全·unix
迪小莫学AI1 小时前
# LeetCode Problem 2038: 如果相邻两个颜色均相同则删除当前颜色 (Winner of the Game)
java·linux·leetcode