多线程(如何理解pthread库)

上一节,我们主要介绍了pthread库中一些常见函数的用法,这节我们主要分析一下pthread库到底是什么?

什么是库

我们之前提过,在每一个linux平台下,必定会存在对应的pthread库

它存在于/lib64这个路径底下

换句话说,库的本质其实就是文件

既然是文件,按照我们之前动态库的知识,我们知道,它需要从磁盘,加载到内存之中,再根据页表,映射到对应的共享区

由于我们不同的执行流,共享的是同一个虚拟地址,因此,同一进程的不同线程都可以随时访问库中的代码和数据

在库中的代码,就有包含诸如线程管理的操作,通过这种方式,我们就可以实现对应的线程切换的操作

具体如何切换呢?
先描述再组织

先描述一个线程,再将不同描述好的线程组织起来,才考虑切换的问题

LWP和TCB

和文件系统类似,在内核级别,我们存在一个file的结构体,并通过数组的方式(文件描述符),对这些结构体进行管理

但是我们进行的操作,都是对struct FIL(C语言),或者fstream对象(C++)进行操作

这是因为操作系统并不信任用户,假如放开权限,让用户自己进行管理,一是所耗费的成本实在是太大了,学习系统级别的操作并不容易;二是极度不安全,用户随随便便不合适的操作就可能导致操作系统的崩溃

因此,我们所学习的操作,都是建立在这之上的,语言级别的操作函数,它的底层会调用相应的诸如open等等系统接口函数

无独有偶

我们之前提到过,在linux系统下,并没有真线程的概念

只有赋用进程结构的轻量级进程LWP

LWP就类似于我们内核中的struct file结构体,我们用户是不能直接操作的

真正的系统调用接口函数是诸如clone这样的函数

linux开发者在这之上,开发了pthread库,通过调用pthread库里面的函数(桥梁),在用户层,我们就可以进行线程的管理;而在操作系统眼里,我们就会调用相应的clone系统调用接口,对轻量级线程LWP进行操作

那在语言层面上进行类比,也应该有类似struct FILE结构体一样的存在

答案是也存在的!

在虚拟内存中(语言层面),我们在共享区会存在对应的struct pthread结构体,我们把它称之为TCB(用户级线程)

至此,我们就初步实现了描述一个线程

在TCB中,存有对应线程的属性,其中就包括我们先前提到的joinable属性,还有每个线程都存在的自己的独立栈空间等等

线程ID

那如何管理不同的线程呢?

文件系统采用的是文件描述符,以数组的方式进行管理,对应的下标就是文件描述符

线程采取的也是类似的方法
不同的线程经过描述后,其实是挨在一起的,就像一个"数组"一样.
因此我们要找到一个用户级线程,实际上,只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息

所谓的线程ID,就是一个地址数据!!!
它用来标识线程相关属性集合(TCB)的起始地址

更进一步来说,就是偏移地址,通过线程ID,我们就能访问不同的线程TCB!

独立栈与线程局部存储

在这之前,我们已经验证过,不同的线程,拥有自己的独立栈

假如每个线程都创建一个cnt变量,并在每次循环的时候,进行对应的减减操作,此时虽然变量名是相同的,但是它们并不是同一个变量

cpp 复制代码
    1 #include <iostream>
    2 #include <pthread.h>
    3 #include <unistd.h>
    4 #include <cstring>
    5 #include <string>
    6 using namespace std;
    7 
    8 int g_val =100;
    9 string toHex(pthread_t tid)
   10 {
   11   char buffer[64];
   12   snprintf(buffer,64,"0x%x",tid);
   13   return buffer;
   14 }
   15 void* pthreadRun(void* args)
   16 {
   17   string name = static_cast<const char*>(args);
   18   int cnt = 5;
   19   while(cnt)
   20   {
   21     cout << name << " : " << cnt-- << ",cnt: " << cnt << endl;                                                                                                    
   22     //cout << name << " g_val: " << g_val++ << ", &g_val: " << &g_val << endl;
   23     sleep(1);
   24   }
   25   return nullptr;
   26 }
   27 int main()
   28 {
   29   pthread_t t1,t2,t3;
   30   pthread_create(&t1,nullptr,pthreadRun,(void*)"thread 1");
   31   pthread_create(&t2,nullptr,pthreadRun,(void*)"thread 2");
   32   pthread_create(&t3,nullptr,pthreadRun,(void*)"thread 3");
   33 
   34   pthread_join(t1,nullptr);
   35   pthread_join(t2,nullptr);
   36   pthread_join(t3,nullptr);
   37   return 0;
   38 }

从输出的结果也可以验证这一点,每个不同的cnt都会进行减1操作

若是相同的cnt,三个线程同时进行减1操作,则很快就会减为0

而cnt在函数内部,是一个临时变量

也侧面印证了我们所有线程都有自己独立的栈结构的观点

那我们原来说的虚拟地址空间中的栈又是什么呢?

之前我们的说法针对的都是拥有一个执行流的进程

现在也是一样,只不过是多个执行流

因此,我们之前所说的,虚拟地址空间中的栈,是进程系统栈,是主线程用的;而新线程用的是库中的栈

其中,下面的这些数据,都是每个线程自己独立拥有的

线程ID

一组寄存器

errno

信号屏蔽字

调度优先级

更深层次讲,为什么每个线程能够自由的切换不同的栈呢?

原因就在于每个线程都有自己独立的一组寄存器,进入函数,实际就是创建对应的函数栈帧,只要更改ebp,esp,我们就能切换线程的栈

C++线程库

这里并不是讲解C++线程库是如何使用的

而是指出,在使用C++线程库的时候,也需要包含pthread.h头文件

假如不包相应的头文件,则g++编译的时候,就会发生报错

原因就在于C++thread库的实现,其实就是对我们上述讲的对原生线程库的封装 ,使用户实现多线程更为便捷

而在使用的时候,我们也是多学习有关语言层次的线程库使用,而比较少用原生线程库

原因就在于,语言是可以跨平台实现的,在封装的时候,就已经充分考虑过平台问题,一段相同的代码,既可以在linux上跑,也可以在windows上跑;但是原生线程库,只适用于Linux系统

相关推荐
hjjdebug2 小时前
linux 下 signal() 函数的用法,信号类型在哪里定义的?
linux·signal
其乐无涯2 小时前
服务器技术(一)--Linux基础入门
linux·运维·服务器
Diamond技术流2 小时前
从0开始学习Linux——网络配置
linux·运维·网络·学习·安全·centos
斑布斑布2 小时前
【linux学习2】linux基本命令行操作总结
linux·运维·服务器·学习
Spring_java_gg2 小时前
如何抵御 Linux 服务器黑客威胁和攻击
linux·服务器·网络·安全·web安全
✿ ༺ ོIT技术༻2 小时前
Linux:认识文件系统
linux·运维·服务器
会掉头发3 小时前
Linux进程通信之共享内存
linux·运维·共享内存·进程通信
我言秋日胜春朝★3 小时前
【Linux】冯诺依曼体系、再谈操作系统
linux·运维·服务器
饮啦冰美式3 小时前
22.04Ubuntu---ROS2使用rclcpp编写节点
linux·运维·ubuntu