上一期我们进行了线程控制的了解与相关操作,但是仍旧有一些问题没有解决
本章第一阶段就是解决tid的问题,第二阶段是进行模拟一个简易线程库(为了加深对于C++库封装linux原生线程的理解),第三阶段就是互斥。
目录
- 线程id:
- LWP与tid:
- 动态库的加载:
- 线程id:
- 如何理解维护在库中:
- 再次感受一下pthread_join():
- 线程局部存储:
- 封装线程库:
- 封装:
- 互斥:
线程id:
LWP与tid:
我们还是先来写一段简单的代码进行验证一下LWP与线程id的关系。
代码:
验证结果:
足以观察到LWP与tid的差距是非常大的。
这说明给用户提供的线程id并不是内核中的LWP,而是自己维护的一个唯一值。
自己就是pthread库。
虽然刚开始觉得不符合常理,但仔细想一想而本该如此:
因为linux并没有线程,但是我们用户需要线程的概念,所以pthread库充当了一个中间角色,封装linux中的轻量级进程,因此,并不需要呈现给用户LWP的值,给用户呈现自己封装的线程id即可。就像C语言中的FILE,我们直接用库封装好的,并不需要在使用文件描述符fd了,也不需要展现给用户。
因为库提供了线程id,所以库也要对pthread进行管理,怎么理解呢?
可以理解为学校给你提供了学号,所以学校要对你进行管理。
我们的linux肯定提供了轻量级进程的调度系统调用,但是一个线程不仅仅需要被调度,也需要一个id,栈大小,被谁启动…这些属性也是由库做管理
的!
针对管理我们要展开一下。
动态库的加载:
那就要先看一下线程库的加载,首先动态库和我们的程序肯定都是在磁盘上的文件。
当我们./
运行时,会建立内核数据结构 + 加载数据与代码。
当我们执行到pthread_create时,因为我们还未加载动态库,会触发缺页中断,去加载动态库,再将动态库映射到共享区。
此时我们就可以正常去执行我们的pthread_create去创建线程了。
而我们也说过库需要对我们的线程id,栈的大小…进行维护,也就是进行管理
而管理就需要对该对象进行描述再进行组织,下图就是描述他的结构体pthread_t。
其中的struct pthread是用户最基本的线程属性,线程局部存储我们稍后再来进行讲解。而编程栈就是我们常说的每个线程都有一个独立的线程栈!
组织我们可以看成是使用一个数组进行组织起来的。
线程id:
所以以后想找线程属性,拥有地址即可进行管理,而我们的tid就是相应的pthread_t的地址。
如何理解维护在库中:
我们还是以FILE进行举例。
我们的FILE是一个结构体:
struct FILE
{
int fd;
char buff[N];
...
}
我们打开一个文件会得到一个FILE的指针。
而这个FILE结构体指针就维护在标准库中,进入这个函数时,会执行malloc(sizeof(struct FILE))
类似的代码在堆上申请空间,等执行完之后返回给用户FILE*,让用户进行操控。所以我们也就可以理解维护在库中了。
就像我们使用STL中的各种容器,不需要管底层是如何扩容的。
再次感受一下pthread_join():
所以我们也就理解了pthread_create时的attr
这就是用来控制pthread_t的属性,比如控制栈的大小…
总结:linux线程 = pthread库 + LWP,其中内核维护的LWP与动态库中维护的线程是1:1
的。
但是同学们,我们该如何保障新线程轻量级进程会使用你指定的栈?
因为我们的轻量级进程中有一个系统调用:clone。
第一个是回调函数,第二个就是指定的栈,第三个是参数,所以pthread库本质就是对这样的一堆系统调用进行封装。
线程局部存储:
现在还剩最后一个问题,线程局部存储是啥?
我们先来看这样一段代码:
搞一个全局变量,新线程改,主线程读取。
现象: 一改具改,符合预期。
但是如果我们想要互相不影响,也就是新主线程虽然用同一个全局变量名字,但是实际却是两个地址。
我们可以加入编译选项__thread
注意:这个只可以修饰全局的内置类型。
现象:可以看到虚拟地址也不相同。
结论:虽然看起来还是用一个,但实际上各自私有一份
封装线程库:
本质是为了更好的理解C++11是如何进行封装的。
我们的目标是实现如下几个接口:
也可以在来一个GetStatus,调用可以观察到此线程是否正在运行。
封装:
先来解释一下成员变量,_name就是线程名字,_tid是线程id,_func是用户传递来的要执行的函数,_isRunning是代表当前线程是否在运行。
在实现时有两个的坑点。
其一:因为我们是进行封装,所以是在类中实现。
但是我们将routine写出了之后却找不到对应routine,这是因为类中的成员默认有隐含的this指针
,所以参数的个数就不匹配了。
解决这种问题的办法很多,但我们最喜欢用static进行修饰,这样就不会有this指针了,这个函数属于整个类。
可是这样我们就无法访问类中的成员变量了,因为没有this指针~
那怎么办?
把this指针当做参数传给routine!
这样就可以调用外部给我们传的指定函数了。
可是这样调用未免有些丑陋。
在将stop,join进行填补即可,注意,我们的构造函数需要一个你指定的名字和待执行函数。
主函数代码:
现象:
也是完成了我们预期的工作。
源代码:
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
namespace cyc
{
class mythread
{
public:
typedef void (*func_t)(std::string);
mythread(const std::string &name, func_t func) : _name(name), _func(func), _isRunning(false)
{
}
~mythread()
{
}
void Excute()
{
_isRunning = true;
_func(_name);
_isRunning = false;
}
static void *routine(void *arg)
{
mythread *self = static_cast<mythread *>(arg);
self->Excute();
return nullptr;
}
void Start()
{
int n = ::pthread_create(&_tid, nullptr, routine, (void *)this);
if (n != 0)
{
perror("pthread_create fail");
exit(1);
}
}
void Stop()
{
if (_isRunning)
{
pthread_cancel(_tid);
_isRunning = false;
}
}
void Join()
{
int n = pthread_join(_tid, nullptr);
if (n != 0)
{
perror("pthread_join fail");
exit(1);
}
}
std::string GetStatus()
{
if(_isRunning) return "Running";
else return "sleeping";
}
private:
std::string _name;
pthread_t _tid;
func_t _func;
bool _isRunning;
};
}
于是我们就完成了一个很简易的封装~
互斥:
多个线程能够看到的资源叫做共享资源。
但是也会有一些问题,比如我们线程通信,一个线程想写hello world,但是刚写了hello就被另一个进程独走了,叫做读写不一致,
所以我们需要对共享资源做保护。
其中最简单的方法是互斥。
但是我们总要先见一见吧。
我们模拟一个抢票的代码,假设一共有1w张票,创建4哥线程同时去抢,每抢一次记录一下抢之前的票数,当票数<=0就是出现了问题。
我们就是用刚刚模拟实现的线程进行操作。
代码:
现象:
果然出现了0甚至负数,这就证明我们的抢票提供非常的失败~
可是原理是什么呢?
首先我们要有两个储备知识。
其一是判断也是一种运算,为逻辑运算。
一共有两种运算,分别为算术运算与逻辑运算,简称算罗运算。
其二是线程的切换,CPU内寄存器只有一套,但是拥有的数据有多套。切换时带走自己的数据,回来时会回复!
对于1来说,逻辑判断至少分为3步,虽然语法上表现为3步,但是实际转换到汇编有2-3步。
我们假设线程1在还有最后一张票时进入,已经判断完毕了,但是此时tickets还没进行--
,这时时间片到了,线程1被切换,带走了当前寄存器的值与记录执行到的语句。同理2,3,4也都分别执行完判断语句就被切换这就有很大的问题了。
因为只有一张票却有4个线程进入了,等线程a,b,c,d分别恢复时将数据又放回到寄存器中。
注意:进行--
时需要将数据重读,修改,在放回内存中这三步
所以票数就变为0,-1,-2…
下章继续~