简单封装线程库 + 理解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说,在虚拟地址空间中,你线程的栈空间就是这么大。

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

相关推荐
不爱学英文的码字机器14 分钟前
[操作系统] 进程程序替换
linux·运维·服务器
韩曙亮19 分钟前
【系统架构设计师】操作系统 - 进程管理 ① ( 进程概念 | 进程组成 | 进程 与 程序 | 进程 与 线程 | 线程 可共享的资源 - ☆考点 )
操作系统·线程·进程·软考·进程管理·程序·系统架构设计师
不想编程小谭28 分钟前
从小白开始的动态规划
c++·算法·动态规划
酥暮沐31 分钟前
LVS集群
linux·服务器·lvs
surtr11 小时前
【C++】RBTree(红黑树)模拟实现
数据结构·c++·算法·stl·map·红黑树·rbtree
zjkzjk77111 小时前
函数指针(Function Pointer)与 typedef int (*FuncPtr)(int, int);typedef与using(更推荐)
开发语言·c++·算法
余辉zmh1 小时前
【动态规划篇】:动态规划解决路径难题--思路,技巧与实例
c++·算法·leetcode·动态规划
一匹电信狗1 小时前
C++引用深度详解
c语言·开发语言·c++·visual studio
kdayjj9661 小时前
从基础到进阶:一文掌握排序、查找、动态规划与图算法的全面实现(C++代码实例解析)
c++·算法·动态规划
阿昊真人1 小时前
node 程序占用处理方法与后台运行方法
linux·编辑器·vim