Linux之多线程

首先我们来讲线程的概念!

首先我们偏官方的解释

线程:是进程内的一个执行分支。线程的执行粒度,要比进程要细。

回忆之前的地址空间,每一个进程都有单独的地址空间,也就是有独立的资源!

如果我们想让多个执行流执行,我们就要开辟父子进程了!

但是如果我们创建新的进程还要创建很多管理数据结构!还要重新开辟相关的地址空间,申请资源等,创建起来非常费时费力!

所以我们就创造了线程的概念,也即在一个进程之内将任务进行分割!(资源和代码都分割)

具体如何分割我们后面将会演示,这里只是粗粒度的看一下!

这样就可以让线程执行更加轻便的任务了。

然后我们再来回忆一下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获取任务是要花时间的,效率比较低,当消费的时候也是要花时间的,不单单只是拿出来就行了,所以多生产多消费的时候的意义在于生产之前,消费之后,处理任务获取任务的时候本身也是要花费时间的,可以在生产之前与消费之后让线程并行执行。

条件变量是一种同步机制,它允许线程等待某个条件的发生,通常与互斥锁一起使用。而信号量是一种计数器,它可以用于控制对共享资源的访问;如果想让每一刻只有一个线程访问共享资源,可以使用条件变量。但如果需要允许多个线程并发访问共享资源的不同区域,则可以使用信号量。

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值