多线程(如何理解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系统

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言