多线程(如何理解pthread库)

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

什么是库

我们之前提过,在每一个linux平台下,必定会存在对应的pthread库
它存在于/lib64这个路径底下
在这里插入图片描述
换句话说,库的本质其实就是文件
既然是文件,按照我们之前动态库的知识,我们知道,它需要从磁盘,加载到内存之中,再根据页表,映射到对应的共享区
由于我们不同的执行流,共享的是同一个虚拟地址,因此,同一进程的不同线程都可以随时访问库中的代码和数据
在库中的代码,就有包含诸如线程管理的操作,通过这种方式,我们就可以实现对应的线程切换的操作
具体如何切换呢?
先描述再组织
先描述一个线程,再将不同描述好的线程组织起来,才考虑切换的问题

LWP和TCB

那如何描述一个线程呢?
之前我们提到过Linux文件系统,在内核级别,我们存在一个file的结构体,并通过数组的方式(文件描述符),对这些结构体进行管理
但是我们用户进行的操作,都是对struct FIL(C语言),或者fstream对象(C++)进行操作
这是因为操作系统并不信任用户,假如放开权限,让用户自己进行管理,一是所耗费的成本实在是太大了,学习系统级别的操作并不容易;二是极度不安全,用户随随便便不合适的操作就可能导致操作系统的崩溃
因此,我们所学习的操作,都是建立在这之上的,语言级别的操作函数,它的底层会调用相应的诸如open等等系统接口函数,这些系统接口函数,会帮助我们将struct FILE与系统底层的struct file建立联系,我们对struct FILE结构体的操作,在底层,OS都会帮我们转变为对struct file结构体的操作.
无独有偶
我们之前提到过,在linux系统下,并没有真线程的概念
只有赋用进程结构的轻量级进程LWP
LWP就类似于我们内核中的struct file结构体,我们用户是不能直接操作的
需要系统接口函数,用户才能访问,诸如clone(Linux 2号手册)这样的函数
在这里插入图片描述
但是,前面我们已经提到过,就算对LWP进行操作,这仅仅是轻量级进程啊,我们用户需要的是线程!就像我需要的鸡肉(线程),你给我的是鱼肉(轻量级进程),就算是龙肉,但我只想吃鸡肉,也没有任何作用
所以,linux开发者在这之上,开发了pthread库,通过调用pthread库里面的函数(桥梁),在用户层,我们就可以进行线程的管理;而在操作系统眼里,我们就会调用相应的clone系统调用接口,对轻量级线程LWP进行操作
那按照前面的说法,在语言层面上进行类比,也应该有类似struct FILE结构体一样的存在?
答案是也存在的!
在虚拟内存中(语言层面),我们在共享区会存在对应的struct pthread结构体,我们把它称之为TCB(用户级线程Thread Control Block)
在这里插入图片描述

至此,我们就初步实现了描述一个线程
在TCB中,存有对应线程的属性,其中就包括我们先前提到的joinable属性,还有每个线程都存在的自己的独立栈空间等等

线程ID

那如何管理不同的线程呢?将不同的线程组织起来
文件系统采用的是文件描述符,以数组的方式进行管理,对应的下标就是文件描述符
线程采取的也是类似的方法
不同的线程经过描述后,其实是挨在一起的,就像一个"数组"一样.
因此我们要找到一个用户级线程,实际上,只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息

所谓的线程ID,就是一个地址数据!!!
它用来标识线程相关属性集合(TCB)的起始地址
更进一步来说,就是偏移地址,通过线程ID,我们就能访问不同的线程TCB!
(就像一个数组一样,访问每一个元素,由于每个元素,它们的虚拟内存地址都是紧挨在一起,只需要首元素地址+对应偏移地址,就可以轻松访问每一个元素)
在这里插入图片描述

独立栈与线程局部存储

在这之前,我们已经验证过,不同的线程,拥有自己的独立栈
假如每个线程都创建一个cnt变量,并在每次循环的时候,进行对应的减减操作,此时虽然变量名是相同的,但是它们并不是同一个变量

    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系统

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值