首先我们来讲线程的概念!
首先我们偏官方的解释
线程:是进程内的一个执行分支。线程的执行粒度,要比进程要细。
回忆之前的地址空间,每一个进程都有单独的地址空间,也就是有独立的资源!
如果我们想让多个执行流执行,我们就要开辟父子进程了!
但是如果我们创建新的进程还要创建很多管理数据结构!还要重新开辟相关的地址空间,申请资源等,创建起来非常费时费力!
所以我们就创造了线程的概念,也即在一个进程之内将任务进行分割!(资源和代码都分割)
具体如何分割我们后面将会演示,这里只是粗粒度的看一下!
这样就可以让线程执行更加轻便的任务了。
然后我们再来回忆一下Linux中是如何调度进程的!
对CPU来说只看task_struct然后执行就可以了。
那么我们Linux就可以利用这一点,每次创建新的线程只需要创建新的task_struct,不开辟新的地址空间,就可以完成新的任务创建!!!
并且这样可以充分的复用进程的task_struct,而不用重新为线程写一套,大幅度的提高了代码的可维护性和复用度!!!
也即Linux没有真正的线程,而是用进程模拟实现线程!!!
这也就是为什么Linux的线程我们一般称为轻量级进程!
(不同操作系统的具体实现方式不一样,这里是Linux的实现方式,非常优雅)
并且从上面的介绍中,我们先推出两个结论!
1.进程是资源管理的基本单位!
2.线程是操作系统调度的基本单位!
然后我们再回忆一下以前对进程的定义
那现在加入了线程,我们又如何理解呢?
那我们先思考一下线程(执行流)是不是资源?
答案是肯定的——是!
所以线程(轻量级进程)对进程来说也是资源!
就像对老板来说,原材料(其他资源)和工人(线程)都是的资源!!!
而我们以前的时候没有引入线程的概念的时候,其实就是进程只有一个线程的时候,反而才是特殊情况!(光杆司令)
接下来我们再来重新深入的讲一下地址空间
首先我们回忆一下,其实物理内存也是被4kb,4kb划分出来的!
(类似于磁盘中的扇区)被称为页框!
首先我们想一下可能是一一映射的关系吗?
页表中包含虚拟地址,物理地址,状态的信息,哪怕保守估计一个映射关系是10个字节,
那总共2^32个映射关系(32位系统)
我们要花接近40G的内存!!!显然根本不可能是这样进行映射的!!!
接下来我们来研究真正的页表构建方法!!!
真正的页表其实是被拆成两级的!
32位地址被转化为10+10+12位
也就是前十位地址被存到一级页表,然后第二个十位存在二级页表,二级页表就可以支持我们找到页框了!然后再通过后十二位我们就可以偏移找到我们的地址了!
这个时候我们再算一下一个页表最大为
相比于之前的40G已经非常少了!
并且我们每个进程并不会用完所有的页表,每个进程都只会创建一部分页表
所以这样页表要的资源对我们来说就不算什么了!
但是这仍然是个比较重的工作,这也就是我们为什么要诞生线程!!!
接下来我们来学习一些周边的小问题
首先为什么线程比进程更轻量化
针对第一个:我们创建线程不用像创建进程那样还要创建页表等,更加轻量化,不用多说。
针对第二点:我们稍微细谈一下!
首先在我们的CPU其实有一个cathe
(一个比较大的缓存空间,相比于内存来说小,但是比cpu的空间大)
比如我们要运行一个代码的第10行我们可能把10-50行的代码和数据储存起来,等下运行的时候就可以直接从cache中调用而不用从内存中去读取这些内容了。这样可以大幅度提高效率,但是大家可能会有疑问,万一我不执行呢,比如函数跳转等!那么我们再想一下在一份代码里面是代码往下执行的概率大,还是函数跳转的概率大?显然函数跳转应该才是少数!
所以这个优化其实是一种基于概率的优化!
所以如果是进程间更替,我们的cache数据(也称热数据)不用被更换(或者需要更新的数据较少,不用整个替换),这样也可以大幅度提高效率,如果进程替换,那么这个数据也就无效了,必须要进行数据的替换,也拖慢运行效率!!!!!!
由此基于上面所说的线程之所以在运行期间更加轻量化于=与cache有非常密切的关系!!!
接下来我们来总结一下线程的优缺点
很多资源线程之间是共享的
再就是线程之间虽然共享资源但也有单独的数据!
一般我们就简化为上下文数据和栈!
接下来我们开始学习使用Linux的线程!
但是Linux并没有关于线程的系统调用!只有轻量级进程的系统调用!
所以为了方便我们的使用,无数程序员就对轻量级进程进行封装写了一个pthread线程库!
(几乎所有的平台都默认自带这个库!!!)
所有接下来我们学习pthread线程库的使用!
我们先来看创建线程的系统调用接口
第一个参数:这是一个输出形参数,线程的管理id
第二个参数:线程的属性,现在我们设置为NULL就可以了
第三个参数:是一个函数指针,返回值和参数类型都为void*,也就是我们要让新线程执行的函数
接下来我们看一下我们的使用实例!
然后我们编译运行就会发现出现了编译报错(链接时报错)!
原因就是因为我们用的pthread库不是c++标准库或者系统调用,而是第三方库!
所以我们根据前面学习动静态库的知识,我们必须要链接pthread库!
然后就可以正常执行了!
接下来我们可以通过ps -aL查看我们的轻量级进程!
这里的LWP就是线程的id!并且主线程的PID=LWP!并且子线程会依次以主线程的LWP向后延伸创造!!!
所以我们以前的调度看似使用的是PID其实是LWP!
(并且只要有一个线程被杀掉,整个进程都会崩溃,所以多线程的健壮性较差)
并且一个函数可以被多个线程所执行,也就在这里我们又看到了函数被重入的实例了!
接下来我们来深入了解一下tid!
我们先打印看一看(先直接打印整形会出现非常大的数,然后猜测是地址)
所以这个并不是简单的返回tid给我们,之后我们再来深入探究!!!
现在我们再思考一个问题,多进程需要等待,那多线程呢?
答案是也要等待,否则也会出现类似僵尸进程一样的东西!
接下来我们直接看等待的函数(进程是wait())
而多线程则是pthread_join!
第一个参数:pthread,不多赘述
第二个参数:为了要获取一个void*的返回值,所以我们要一个void**的参数去接收变量。
这个retval就被接收到了!
然后这个地方提一下,如果线程是被取消的,就会返回默认值-1
我们在这里稍微提一下语言的可移植性
首先我们c++11之后便加入了线程库,但是你会发现仍然需要链接-pthread库
所以也就是c++的线程库其实也是对pthread的封装!
由此移植性好,其本质就是外面的封装库不变,而内部调用的函数(系统调用)方便替换就可以算作可移植性好。
比如我们的phtread_creat,join....都是对clone的封装。
由此,我们就要明确线程的概念是库给我们维护的(不是维护线程的执行流),也即所谓的独立栈什么的,其实都是线程库里面维护的数据结构而已,所以才能在同一地址空间共享空间。
于是我们的库就要维护这些线程,那么如何维护了——先描述再组织!于是就存在线程控制块!
所以我们就可以来看看tcb(线程控制块)了!
1.每一个线程的tcb起始地址就是tid!
2:除了主线程,所有其他线程的独立栈,都在共享区具体来讲是在pthread库中,tid指向的用户tcb中!
3:线程局部存储
可以将全局变量给每个线程保存一份!
只要加上__thread就可以启动线程局部存储了。
这样就可以有一份线程级别的全局变量了!(比如在线程里面调用函数要查看当前线程的ID,如果不这样就要一直传,如果这样函数就可以直接访问这个全局变量了,编译器会自动调用访问该线程的该全局变量)
看完了tcp,我们再来了解一下pthread_detach
正常一个线程是需要join的,但是如果自己对自己detach之后,线程结束以后就会自动分离,而不需要等待。
学完上面的基础概念之后
我们来写一个多线程抢票的程序
然后我们就发现出现了意料之外的结果,我们明明判断了tickets>0才能抢,现在居然把票抢到负数了!!!
而产生的原因就是多执行流访问共享数据造成的数据不一致问题!!!
所以我们就产生了问题——对一个全局变量进行多线程并发--/++操作是否是安全的?
答案很明显——不安全!为什么呢?因为多线程在进行访问的时候必须是线性的,所以在执行if判断的时候,必须先把ticket变量的数据从内存中先放到cpu寄存器,但是也就是在寄存器的过程中,这时时间片可能结束了,于是线程就暂时停止运行了,其他线程也可能就进行了操作覆盖了内存中的数据,此时线程再恢复运行的时候执行ticket--时,重新读取新的数据进行--就会导致这种意料之外的问题发生!
所以就有可重入函数和不可重入函数(也即能被多执行流并发访问没有问题的函数和有问题函数)
于是为了防止不可重入函数被重入由此我们就引出了————线程锁!
我们首先来看看线程锁的接口!
实际用法如下!就是创建锁,初始化锁,销毁锁的过程。
然后就是加锁和解锁的函数调用。
大家如果自己去尝试这个例子,如果把usleep删去,大部分票都会被一个线程抢走,所以其实也就说明了线程之间的竞争能力是不同的!!!(有强有弱)
但是一般我们解锁以后一般会有动作,否则就要加以限制,否则会导致其他线程抢不到锁
也叫锁饥饿问题!所以我们在使用锁的时候一定要注意这个问题!
而比饥饿问题危害性更大的问题就是死锁问题!
首先我们来看产生死锁的四个必要条件(产生死锁一定有这些条件,有这些条件不一定有锁)
所以就像防止饥饿问题一样,我们现在如果想避免死锁,就可以尝试破坏其中的条件!!!
所以我们就有一种方法去解决这个问题——条件变量!
用法几乎一样,但是却可以增加条件变量!
其中braodcast是唤醒所有,signal是唤醒一个!
可能大家不是很理解,我们接下来通过实例来理解!
当在等待的时候就会放开锁直到通知唤醒!比如这个程序就可以让每个线程轮流唤醒再等待!
但是这个地方我们暂时没有例子,不容易理解,由此我们就可以引入我们的生产消费者模型了!
CP问题————consumer produecer(生产者消费者模型)
我们在日常生活中,我们买东西从来都是在超市而不是去找工厂买!为什么呢?
效率高!!!
同样,我们在计算机当中也是这样,如果有一个类似超市的缓冲区,那么生产和消费就可以不同步了!可以达到忙闲不均!这样工厂不用等待消费者购买的时候才去生产,消费者购买的时候不用供货商的生产,从而提高效率!
在计算机里面也是一样的!如果我们一部分线程充当工厂,一部分用来充当消费者!
比如在网络部分,一部分线程用来接受数据,一部分用来处理数据,就是cp模型的体现!!!
整个来说就是3种关系,2种角色,1个中间场所!简称321原则!
在这里我们就可以将以前的信号量进一步讲解了!
之前我们就知道信号量可以控制线程对相应资源的访问数量
信号量概念
什么是信号量?
只要我们对资源进行整体加锁就默认了我们对这个资源整体使用,实际情况可能存在一份公共资源,但是允许同时访问不同的区域!(程序员编码保证不同的线程可以并发访问公共资源的不同区域!)
信号量本质是一把计数器,衡量临界资源中资源数量多少的计数器
只要拥有信号量,就在未来一定能够拥有临界资源的一部分,申请信号量的本质:对临界资源中特定小块资源的预定机制。比如电影院买票预定座位
只要申请成功,就一定有你的资源,只要申请失败,就说明条件不就绪,你只能等,就不需要判断
线程要进行访问临界资源中的某一区域——得先申请信号量——前提是所有人必须先看到信号量——所以信号量本身必须是:公共资源。
这是我们之前学习的相关知识,大家可以先复习了一下。
今天我们就可以来学习和了解一下他的接口了!
信号量PV操作
P操作
:sem–,申请操作,必须保证操作的原子性
V操作
:sem++,归还资源,必须保证操作的原子性
#include <semaphore.h>
//信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value)
//sem:自己定义的信号量变量
//pshared:0表示线程间共享,非零表示进程间共享。
//value:信号量初始值(资源数目)。
//信号量销毁
int sem_destroy(sem_t *sem)
//信号量等待
int sem_wait(sem_t *sem):p操作,--
//信号量发布
int sem_pos(sem_t *sem):V操作,++
访问环形队列
生产者和消费者访问同一个位置的情况:空的时候,满的时候;其他情况下生产者与消费者访问的就是不同的区域了。
为了完成环形队列的生产消费,我们的核心工作就是
1.消费者不能超过生产者
2.生产者不能套消费者一个圈以上
3.生产者和消费者指向同一个位置时,如果此时满了就让消费者先走,如果此时为空就让生产者先走
大部分情况下生产者与消费者是并发执行的,但是当环形队列为空或为满的时候就会存在着同步与互斥问题。
如何去进行保证:信号量维护,信号量是衡量临界资源中资源数量的
资源是什么:
1.对于生产者,看中的是队列中的剩余空间,空间资源定义成一个信号量
2.对于消费者,看中的是队列中的数据资源,数据资源定义成一个信号量
比如我们一共有10个位置,消费者初始信号量是0,生产者初始信号量是10,如果生产者线程生产数据,申请信号量,进行P操作,信号量变为9,申请失败则阻塞;申请成功后消费者线程看到了多一个数据资源,消费者信号量进行V操作.所以我们并不需要进行判空判满:当生产者生产满了,信号量申请不到,进行阻塞,只能让消费者先走;当消费者消费完了,信号量申请不到,只能让生产者先走。
代码实现
单生产单消费的环形队列生产者消费者模型,利用随机数生成数据资源,通过生产线程与消费线程进行数据的生成与数据的消费
实现如下
Task.hpp:
#pragma once
#include <iostream>
#include <functional>
#include <cstdio>
#include <cstring>
class Task
{
using func_t = std::function<int(int,int,char)>;
public:
Task()
{}
Task(int x,int y,char op,func_t func)
:_x(x),_y(y),_op(op),_callback(func)
{}
std::string operator()()
{
int result = _callback(_x,_y,_op);
char buffer[1024];
snprintf(buffer,sizeof buffer,"%d %c %d = %d",_x,_op,_y,result);
return buffer;
}
std::string toTaskString()
{
char buffer[1024];
snprintf(buffer,sizeof buffer,"%d %c %d = ?",_x,_op,_y);
return buffer;
}
private:
int _x;
int _y;
char _op;
func_t _callback;
};
const std::string oper = "+-*/%";
int mymath(int x,int y,char op)
{
int result = 0;
switch (op)
{
case '+':
result = x + y;
break;
case '-':
result = x - y;
break;
case '*':
result = x * y;
break;
case '/':
{
if (y == 0)
{
std::cerr << "div zero error!" << std::endl;
result = -1;
}
else
result = x / y;
}
break;
case '%':
{
if (y == 0)
{
std::cerr << "mod zero error!" << std::endl;
result = -1;
}
else
result = x % y;
}
break;
default:
break;
}
return result;
}
Main.cc
#include "RingQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
void*ProductorRoutine(void*rq)
{
RingQueue<Task>*ringqueue = static_cast<RingQueue<Task>*>(rq);
while(true)
{
//构建任务
int x = rand()%10;
int y = rand()%5;
char op = oper[rand()%oper.size()];
Task t(x,y,op,mymath);
//生产任务
ringqueue->Push(t);
std::cout<<"生产者派发了一个任务:"<<t.toTaskString()<<std::endl;
//sleep(1);
}
}
void*ConsumerRoutine(void*rq)
{
RingQueue<Task>*ringqueue = static_cast<RingQueue<Task>*>(rq);
while(true)
{
//构建任务
Task t;
//消费任务
ringqueue->Pop(&t);
std::string result = t();
std::cout<<"消费者消费了一个任务:"<<result<<std::endl;
sleep(1);
}
}
int main()
{
srand((unsigned int) time(nullptr)^getpid()^pthread_self()^0x7432);
RingQueue<Task>*rq = new RingQueue<Task>();
pthread_t p,c;
pthread_create(&p,nullptr,ConsumerRoutine,rq);
pthread_create(&c,nullptr,ProductorRoutine,rq);
pthread_join(p,nullptr);
pthread_join(c,nullptr);
delete rq;
return 0;
}
RingQueue.hpp
#pragma once
#include <iostream>
#include <vector>
#include <cassert>
#include <semaphore.h>
#include <pthread.h>
static const int gcap = 5;
template<class T>
class RingQueue
{
private:
void P(sem_t&sem)
{
int n =sem_wait(&sem);
assert(n==0);
(void)n;
}
void V(sem_t&sem)
{
int n = sem_post(&sem);
assert(n==0);
(void)n;
}
public:
RingQueue(const int&cap = gcap):_queue(cap),_cap(cap)
{
int n = sem_init(&_spaceSem,0,_cap);
assert(n == 0);
n = sem_init(&_dataSem, 0, 0);
assert(n==0);
_productorStep = _consumerStep = 0;
pthread_mutex_init(&_pmutex,nullptr);
pthread_mutex_init(&_cmutex,nullptr);
}
void Push(const T&in)
{
P(_spaceSem);//申请到了空间信号量,意味着我们一定能进行正常的生产
pthread_mutex_lock(&_pmutex);
_queue[_productorStep++] = in;
_productorStep%=_cap;
pthread_mutex_unlock(&_pmutex);
V(_dataSem);
}
void Pop(T *out)
{
P(_dataSem);
pthread_mutex_lock(&_cmutex);
*out = _queue[_consumerStep++];
_consumerStep%=_cap;
pthread_mutex_unlock(&_cmutex);
V(_spaceSem);
}
~RingQueue()
{
sem_destroy(&_spaceSem);
sem_destroy(&_dataSem);
pthread_mutex_destroy(&_pmutex);
pthread_mutex_destroy(&_cmutex);
}
private:
std::vector<T> _queue;
int _cap;
sem_t _spaceSem;
sem_t _dataSem;
int _productorStep;
int _consumerStep;
pthread_mutex_t _pmutex;
pthread_mutex_t _cmutex;
};
Main.cc
#include "RingQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
std::string SelfName()
{
char name[128];
snprintf(name,sizeof(name),"thread[%0x%x]",pthread_self());
return name;
}
void*ProductorRoutine(void*rq)
{
RingQueue<Task>*ringqueue = static_cast<RingQueue<Task>*>(rq);
while(true)
{
int x = rand()%10;
int y = rand()%5;
char op = oper[rand()%oper.size()];
Task t(x,y,op,mymath);
//生产任务
ringqueue->Push(t);
std::cout<<SelfName()<<",生产者派发了一个任务:"<<t.toTaskString()<<std::endl;
//sleep(1);
}
}
void*ConsumerRoutine(void*rq)
{
RingQueue<Task>*ringqueue = static_cast<RingQueue<Task>*>(rq);
while(true)
{
Task t;
//消费任务
ringqueue->Pop(&t);
std::string result = t();
std::cout<<SelfName()<<",消费者消费了一个任务:"<<result<<std::endl;
sleep(1);
}
}
int main()
{
srand((unsigned int) time(nullptr)^getpid()^pthread_self()^0x7432);
RingQueue<Task>*rq = new RingQueue<Task>();
pthread_t p[4],c[8];
for(int i = 0;i<4;i++) pthread_create(p+i,nullptr,ProductorRoutine,rq);
for(int i = 0 ;i<8;i++) pthread_create(c+i,nullptr,ConsumerRoutine,rq);
for(int i = 0;i<4;i++) pthread_join(p[i],nullptr);
for(int i = 0 ;i<8;i++) pthread_join(c[i],nullptr);
return 0;
}
总结
多生产多消费的意义:不管是环形队列还是阻塞队列,多线程的意义在于构建or获取任务是要花时间的,效率比较低,当消费的时候也是要花时间的,不单单只是拿出来就行了,所以多生产多消费的时候的意义在于生产之前,消费之后,处理任务获取任务的时候本身也是要花费时间的,可以在生产之前与消费之后让线程并行执行。
条件变量是一种同步机制,它允许线程等待某个条件的发生,通常与互斥锁一起使用。而信号量是一种计数器,它可以用于控制对共享资源的访问;如果想让每一刻只有一个线程访问共享资源,可以使用条件变量。但如果需要允许多个线程并发访问共享资源的不同区域,则可以使用信号量。