多线程II

多线程—II

线程安全
  • 多个线程同一时刻对同一个全局变量(同一份资源)做写操作(读操作不会涉及线程安全)时,如果跟我们预期的结果一样,我们就称之为线程安全,反之,线程不安全

  • 线程安全是保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况

  • 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全,如果说同时进行读写操作的话,可能就会导致线程不安全的现象发生

线程不安全的经典现象—抢票程序
  • 假设说现在从一个地方去到另一个地方的车票只有一张了,这个时候有很多人都要去抢这一张票,但是在这种情况下只有一个人可以抢到这张票,这个时候其实多个线程就扮演的一个角色其实就是“黄牛”的角色,他们都是用来抢这一张票的,但是当程序真正在运行起来的时候,多个线程是抢占式执行的,如果这个线程拿到了,那个线程也拿到了的话,那么就可能出现一定的问题
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define THREADCOUNT 4      //当前有几个线程就相当于是有几个黄牛
 
int g_tickets = 100;       //定义票的数目

//线程的入口函数
void* MyStartThread(void* arg)
{
	//这个arg我们目前没用用到,就利用墙砖不让他获取到就好了
    (void)arg;
    while(1)
    {
    	//当前这个线程就可以理解为他一直在进行抢票的工作
    	//现在去判断到底有没有票
        if(g_tickets > 0)
        {
        	//这一句话就是用来展示,我是谁,并且我抢到了那张票
            printf("i am thread %p, i have ticket %d\n", pthread_self(), g_tickets);
            //如果强到票了,就让我们对票的数目进行--的操作
            g_tickets--;
        }
        else
        {
            break;
        }
    }
    return NULL;
}

int main()
{
	//给出一个数组,循环去创建线程
    pthread_t tid[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
        int ret = pthread_create(&tid[i], NULL, MyStartThread, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
    }
    
	//防止发生内存泄露的问题
    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(tid[i], NULL);
    }
    return 0;
}
  • 像上面那样我们给出四个线程的时候,由运行结果其实就可以看出来,有两个不一样的线程都说自己同时抢到了同时一张票,那么在这种情况下,其实就会导致线程的不安全现象发生
    在这里插入图片描述
    在这里插入图片描述
线程不安全的原理(解释一下什么是线程不安全)
  • 首先需要说结论,线程不安全会导致程序结果出现二义性,然后现在来举个例子说明一下,假设现在有两个线程,由线程A,和线程B,并且有一个int类型的全局变量,值为10,线程A和线程B在各自的入口函数当中对这样一个全局变量进行++的操作,那么具体加的时候怎么去进行加操作呢;线程A拥有CPU之后,对全局变量进行++的操作,但是这个操作其实并不是原子操作,也就是意味着线程A在执行加的过程中很有可能会被打断,假设线程A刚刚将我们的全局变量10读到CPU的寄存器中,这个时候就被切换出去了,程序计数器当中保存了下一条执行的指令,上下文信息当中保存了寄存器的值,这两个东西是用来当线程A再次拥有CPU的时候,回复现场使用的;但是呢,这会线程B很有可能拥有了CPU资源,对全局变量进行了++操作,并且将10加成了11回写到了内存里面,这时候,线程A再次拥有了CPU资源之后,恢复现场,继续向下执行,从寄存器里面读到的值仍然是10,加完之后为11,会写到内存当中也是11;总结下来就是:理论上,线程A和线程B各自对全局变量进行了++的操作,理论上全局变量的值应该变成12,但是,现在程序计算的结果有可能是11,所以这就是线程不安全

  • 操作被打断其实就是进行了进程切换的操作

  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

  • 原子性—指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行
    在这里插入图片描述

那么,如何解决线程不安全的现象呢?
互斥
  • 任何时刻,互斥保证有且只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用
  • 同一时刻,只允许一个执行流去访问临界资源
  • 临界资源:多个执行流都能访问到的资源,就称为临界资源(多线程执行流共享的资源就叫做临界资源)
  • 临界区:访问临界资源的代码块被称为临界区(:每个线程内部,访问临界资源的代码,就叫做临界区)
那么,怎么保证互斥呢?----通过互斥锁
  • 其实我们利用到了互斥锁去保证互斥

  • 互斥锁保证了多个线程在同一时刻只有一个线程可以去访问这块资源

  • 互斥锁本身也是一个资源,我们也需要代码当中来获取互斥锁,我们现在需要用到互斥锁去保证不同的执行流进行互斥(用互斥锁保证不同的执行流之间是互斥的),一定都注意只要多个线程想要保证互斥,那么他们都需要去获取互斥锁,否则的话就无法保证互斥(如果要限制多个线程对某一个临界资源的访问必须加同一把锁,而且每个线程都需要去加锁,如果线程A加了锁,线程B没有加锁的话,那么其实就相当于你没有进行加锁的操作)

互斥锁
  • 互斥锁的本质:在互斥锁内部当中其实是有一个计数器的,这个计数器其实就是互斥量,计数器的取值只能为0或者为1

  • 当线程获取互斥锁的时候,如果当计数器中的值为0的时候,表示当前线程获取不到互斥锁,那么他既然获取不到互斥锁,就不要再去访问临界资源了,也就是说不能去执行临界区当中的代码了

  • 当线程获取互斥锁的时候,如果当计数器中的值为1的时候,表示当前线程获取到了互斥锁,那么他既然获取到了互斥锁,就可以去访问临界资源了,也就是说可以去执行临界区当中的代码了

  • 那么计数器当中的值如何保证我们的原子性呢?

  • 问题其实就是:为什么计数器当中的值从0变成1或者说从1变成0是原子操作

  • 计数器当中的值势必是存在在内存当中的,互斥锁是在我们的内存当中创建出来的一个互斥锁,如果说计数器的值会发生变化的话,那么他势必是会通过CPU的计算才能将他的值去改变掉的,那么也就是说我们现在的计数器当中的值他有可能是0也有可能是1,如果他现在要去发生变化的话,那么他肯定是要和CPU打交道的,同时他需要放入到寄存器当中,那么如何保证原子性呢?就是在获取锁资源的时候(也就是说在加锁的时候,他会先去把寄存器当中的值首先先赋值称为0,然后将寄存器当中的值和计数器当中的值进行交换,然会去判断寄存器当中的值,得出加锁的结果
    在这里插入图片描述

  • 两种情况的初始情况
    在这里插入图片描述

  • 两种情况交换完毕之后的情况
    在这里插入图片描述

  • 那么在交换完毕之后,我们程序当中只需要判断寄存器当中的值就可以了,当寄存器当中的值为1的时候,表示可以加锁,当寄存器当中的值为0的时候,表示不可以加锁,因为此时计数器的值已经为0了

  • xchgb是一个交换操作,这个交换操作他其实是一个汇编代码,汇编指令直接一次性就完成了,所以这个操作是一个原子性的操作
    在这里插入图片描述

互斥锁的接口(真正的加锁和解锁的接口)
初始化互斥锁变量
  • 初始化互斥锁变量一共有两种方式,一种是动态初始化,一种是静态初始化

  • 静态初始化的时候直接采用的就是一个宏,宏的名称如下所示
    在这里插入图片描述

  • 那么这个宏定义到底是长什么样子的呢,互斥锁变量的类型其实是一个结构体类型的东西
    在这里插入图片描述

  • 这个宏定义其实就是一堆值,这一堆值是赋值给结构体的
    在这里插入图片描述

  • 一般情况下我们都采用的是动态初始化
    在这里插入图片描述

加锁
第一种
  • 在进行加锁的时候需要把互斥锁变量传进来
    在这里插入图片描述
    在这里插入图片描述

  • 如果mutex当中的计数器的值为1,则pthread_mutex_lock接口就返回了,表示说加锁成功了,同时计数器当中的值会被更改为0,加锁成功之后,计数器的值会从1变成0;

  • 如果mutex当中的计数器的值为0,则pthread_mutex_lock接口就被阻塞了pthread_mutex_lock接口就没有返回,阻塞在该函数内部,直到加锁成功,会被阻塞的,知道加锁成功

第二种
  • 尝试进行加锁操作的接口
    在这里插入图片描述
  • 这个接口是非阻塞加锁接口
  • 当互斥锁变量当中的计数器的值为1,则加锁成功返回
  • 当互斥锁变量当中的计数器的值为0,我现在是加不了锁的,但是这个接口也给我返回回来了,但是返回回来了,你也需要知道,你现在是一个并没有拿到互斥锁的状态,所以你也是不可以去访问临界资源的,否则的话就会出现问题的,就算是返回回来了,你也需要知道,其实是没有加锁成功然后返回回来的
    在这里插入图片描述
  • 还有需要注意的一点就是非阻塞接口一般都要搭配循环来进行使用的,也就是说当我第一次去尝试加锁的时候,我并没有加锁成功,然后我现在返回了,那么我就需要下次再来继续进行加锁的操作
第三种加锁—带有超时时间的接口

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

解锁

在这里插入图片描述

销毁互斥锁
  • 针对的时动态初始化的互斥锁
  • *pthread_mutex_destroy(pthread_mutex t)
那么我们现在需要对抢票程序去进行更改的操作,防止多个线程抢占到了同一个资源的问题
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define THREADCOUNT 4

int g_tickets = 100;
pthread_mutex_t lock_;   //定义了一个全局变量之后就是所有的线程都可以访问到的东西了

void* MyStartThread(void* arg)
{
    (void)arg;
    while(1)
    {
        pthread_mutex_lock(&lock_);
        if(g_tickets > 0)
        {
            printf("i am thread %p, i have ticket %d\n", pthread_self(), g_tickets);
            g_tickets--;
        }
        else
        {
            pthread_mutex_unlock(&lock_);
            break;
        }
        pthread_mutex_unlock(&lock_);
    }
    return NULL;
}

int main()
{
	//进行初始化的操作
    pthread_mutex_init(&lock_, NULL);
    pthread_t tid[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
        int ret = pthread_create(&tid[i], NULL, MyStartThread, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
    }

    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(tid[i], NULL);
    }

	
	//在要退出的时候对这个锁进行销毁的操作其实就可以了
    pthread_mutex_destroy(&lock_);
    return 0;
}

在这里插入图片描述

同步
  • 同步是指用于实现控制多个进程按照一定的规则或顺序访问某些系统资源的机制,为了在互斥的基础上追求资源分配的合理性,多个东西想要利用同一个资源的时候,还是根据的是抢占式去执行的

  • 假设现在有这么一个场景,现在你呢,需要去买口罩,在这个时候也有很多其他的人都希望去买口罩,药店是销售口罩的地方,但是药店并不是真正意义上生产口罩的地方,真正意义上生产口罩的地方其实是工厂,假设说现在有很多人都来买口罩,但是药店里面现在就只剩下一个口罩了,如果现在我们不保证互斥的前提下,那么很多人可能就都拿到这个口罩了,那势必是不产生矛盾的,所以我们刚才提出了使用互斥锁来解决这个矛盾,在同一时刻只有一个人可以拿到这个口罩,就可以很好的解决矛盾的这个问题了

  • 那还有一种情况就是,闲杂有很多人要来买口罩,但是现在药店里面是没有口罩的,那么药店就会去联系工厂说给药店再提供一批口罩,然后工厂把口罩给药店,药店会再把口罩给我们,但是现在有可能是没有口罩的,那么现在会怎么办呢?就有可能药店会让这些买药的人先离开,留下自己的联系方式,等到有口罩的时候再通知他们过来买口罩,那么当我通知你们的时候,谁来的快,这个东西当然就归谁所有了

  • 条件变量:条件变量的本质其实就是PCB等待队列+唤醒接口

  • pcb等待队列里面放的就是每一个线程的pcb,在条件变量里面有pcb等待队列

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREADCOUNT 2

int g_bowl = 0;
pthread_mutex_t g_mut;
pthread_cond_t g_cond;

void* EatStart(void* arg)
{
    while(1)
    {
        //eat
        pthread_mutex_lock(&g_mut);
        while(g_bowl <= 0)
        {
            pthread_cond_wait(&g_cond, &g_mut);
        }
        printf("i am %p, i eat %d\n", pthread_self(), g_bowl);
        g_bowl--;
        pthread_mutex_unlock(&g_mut);
        //通知做面的人做面
        pthread_cond_signal(&g_cond);
    }
    return NULL;
}

void* MakeStart(void* arg)
{
    while(1)
    {
        //make
        pthread_mutex_lock(&g_mut);
        while(g_bowl > 0)
        {
            //等待
            pthread_cond_wait(&g_cond, &g_mut);
        }
        g_bowl++;
        printf("i am %p, i make %d\n", pthread_self(), g_bowl);
        pthread_mutex_unlock(&g_mut);
        pthread_cond_signal(&g_cond);
    }
    return NULL;
}

int main()
{
    pthread_mutex_init(&g_mut, NULL);
    pthread_cond_init(&g_cond, NULL);
    pthread_t consume[THREADCOUNT], product[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
    	//创建一个消费者
        int ret = pthread_create(&consume[i], NULL, EatStart, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
		
		//创建一个生产者
        ret = pthread_create(&product[i], NULL, MakeStart, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
    }

    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(consume[i], NULL);
        pthread_join(product[i], NULL);
    }

    pthread_mutex_destroy(&g_mut);
    pthread_cond_destroy(&g_cond);
    return 0;
}

在这里插入图片描述

思考的两个问题
为什么会有互斥锁?
  • 为什么会有互斥锁—因为同步并没有保证互斥,而保证互斥时要使用互斥锁的;pthread_cond_wait内部会进行解锁互斥锁的操作,那么,现在问题又来了,到底是像放到PCB等待队列里面还是先进行解锁操作呢?—一定是先放到PCB队列当中,在进行解锁
    在这里插入图片描述
    在这里插入图片描述
死锁
第一种死锁的情况
  • 当多个执行流使用同一个互斥锁的时候,有一个执行流获取到了互斥锁之后,但是没有释放互斥锁,导致其他执行流都卡死在加锁的接口当中,我们称之为死锁

  • 我们在排查思锁问题的时候一定要去排查到底是哪个执行流把这个锁拿走了

  • 发生死锁的话,就是有一个线程拿到了互斥锁,他拿到了互斥锁也就算了,重点是他并没有对互斥锁进行释放的操作,那么这个时候别的线程也就都拿不到锁了,那么这个时候其实就会造成死锁现象的发生,那么我们如何去排查到底是哪个线程拿到了这个互斥锁没有去释放它呢?—我们可以去查看多线程的调用堆栈,我们可以使用thread apply all bt 去查看多线程的调用堆栈

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define THREADCOUNT 4

int g_tickets = 100;
pthread_mutex_t lock_;

void* MyStartThread(void* arg)
{
    (void)arg;
    while(1)
    {
        pthread_mutex_lock(&lock_);
        if(g_tickets > 0)
        {
            printf("i am thread %p, i have ticket %d\n", pthread_self(), g_tickets);
            g_tickets--;
            sleep(5);
            int* lp = NULL;
            *lp = 10;
        }
        else
        {
            //pthread_mutex_unlock(&lock_);
            break;
        }
        //pthread_mutex_unlock(&lock_);
    }
    return NULL;
}

int main()
{
    pthread_mutex_init(&lock_, NULL);
    pthread_t tid[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
        int ret = pthread_create(&tid[i], NULL, MyStartThread, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
    }

    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(tid[i], NULL);
    }

    pthread_mutex_destroy(&lock_);
    return 0;
}
  • 上面的程序里面其实是由5个线程的,一个主线程,四个工作线程
  • 其实如果上面的程序不加sleep(5)那一句话的话,代码运行的结果并不是像下面这个样子的,而是你再启用程序调试之后,你想要去看线程的堆栈,你会发现线程堆栈中有一堆的问号,而不是像下面这样子堆栈中都是正常的代码,那么为什么不加sleep(5)就会有一堆问号呢?之所以有问号,原因就在于你这个堆栈太混乱了(问号表示你通过gdb查看内存的时候其实是一团糟的状态),系统也没有办法给你分辨出来到底什么是什么,那么为什么会混乱呢?因为现在在主线程中要去创建四个工作线程,那么就会去调用4次线程创建函数,四个线程都要向各自的栈中压入线程入口函数,然后第一个线程在被创建出来的时候,他会很快的去调用自己的线程入口函数,然后入口函数又很快的去加锁然后没有解锁就崩溃了,其他的线程都还没调用各自的函数入口函数呢,代码就崩溃了,随意就是看不到他们个自己的内存的
    在这里插入图片描述
  • 那么如何解决上述的问题呢,加一个sleep(5),让他过一会再进行访问空指针的操作(段错误发挥的信号是11号信号),然后代码在崩溃
    在这里插入图片描述
  • 程序的运行结果如下所示:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 那么,我们如何查看到底是哪个线程把互斥锁拿走了呢?我们可以使用p+互斥锁变量的名称,从而去查看到底是哪个线程把互斥锁拿走了,owner后面其实就是轻量级进程号,其实就是线程号
    在这里插入图片描述
    在这里插入图片描述
  • 看到线程号之后往上翻,看看到底是哪个线程把互斥锁拿走了
    在这里插入图片描述
第二种死锁的情况
  • 假设我现在有两把互斥锁,然后现在也有两个执行流,假设现在互斥锁1被线程A拿走了,那么现在其实就是相当于加锁成功了,互斥锁2被线程B拿走了,现在也相当于是加锁成功了,那么在现在这种情况下,假设线程A还想去申请互斥锁2,线程B也想去申请互斥锁1,但是现在互斥锁2被线程B拿着呢,线程B不释放互斥锁2,互斥锁2是不会被线程A拿走的

  • 那既然线程A是拿不到的话那么他其实就被阻塞掉了,线程B是同样的道理,也就是说我们的线程A和线程B都阻塞掉了,那么这个时候整个程序就不会继续向下运行的

  • 多个执行流,多个互斥锁的情况下,每一个执行流都占有一把互斥锁,但是还要去申请对方的互斥锁,这种情况下,就会导致各个执行流都阻塞掉,这种现象称之为死锁

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define THREADCOUNT 1

//在这个时候,我们是需要两把互斥锁的

pthread_mutex_t lock_;
pthread_mutex_t lock2_;

void* ThreadLock1(void* arg)
{
    (void)arg;
    pthread_mutex_lock(&lock_);

    sleep(2);
    
    pthread_mutex_lock(&lock2_);

    pthread_mutex_unlock(&lock2_);
    pthread_mutex_unlock(&lock_);

    return NULL;
}

void* ThreadLock2(void* arg)
{
    (void)arg;
    pthread_mutex_lock(&lock2_);

    sleep(2);

    pthread_mutex_lock(&lock_);

    pthread_mutex_unlock(&lock_);
    pthread_mutex_unlock(&lock2_);
    return NULL;
}

int main()
{
    pthread_mutex_init(&lock_, NULL);
    pthread_mutex_init(&lock2_, NULL);
    pthread_t tid[THREADCOUNT], tid2[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++)
    {
        int ret = pthread_create(&tid[i], NULL, ThreadLock1, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }

        ret = pthread_create(&tid2[i], NULL, ThreadLock2, NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
        
    }

    sleep(3);

    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(tid[i], NULL);
        pthread_join(tid2[i], NULL);
    }

    pthread_mutex_destroy(&lock_);
    pthread_mutex_destroy(&lock2_);
    return 0;
}
  • 代码运行结果如下所示:就是说我们当前的程序其实并不能直接去结束的
    在这里插入图片描述
  • 调试起来可以看到,两把锁被不同的线程所拥有着
    在这里插入图片描述
    在这里插入图片描述
  • 关于调试指令
    在这里插入图片描述
死锁的必要条件:
  • 互斥条件
  • 请求与保持条件(吃着碗里的,看着锅里的)
  • 不可剥夺条件(线程A加的锁,只能线程A释放,不能别人去释放,线程B加的锁,只能线程B释放,不能别人去释放)
  • 循环等待(形成环路等待)
如何预防死锁
  • 破坏必要条件(要么哦哦坏循环等待,要么破环请求与保持)
  • 加锁顺序一致
  • 不要忘记解锁,在所有可能导致进程流退出的地方都进行解锁
解决死锁的基本方法
预防死锁

在这里插入图片描述

生产者和消费者模型
  • 涉及到了一个123规则—1个线程安全的队列(队列的特性是先进先出,所有满足先进先出特性的结构体我们都可以称之为队列),2种角色的线程,3种关系(生产者和生产者互斥,消费者和消费者互斥,生产者和消费者同步加互斥
  • 当然,我们也需要保证线程安全这个东西,也就是说,我们需要保证在同一时刻,队列当中的元素只有一个执行流去访问,只有一个执行流的意思就是我是不去进行区分的,就是说我不对其进行区分,区分他到底是生产者流还是消费者流,这个我是不进行区分的
  • 那么如何去保证下称安全呢,我们就可以通过互斥锁和条件变量去确保线程的安全
为什么要有一个生产者和消费者模型来控制我们的程序呢?
优点:
  • 支持忙先不均
  • 生产者与消费者可以互相不干扰
  • 支持高并发
    在这里插入图片描述
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
#include <queue>

#define THREADCOUNT 2

//生产者与消费者模型
//   1.线程安全的队列
//      std::queue
//      互斥: pthread_mutex_t
//      同步:pthread_cond_t
//   2.两种角色的线程
//      生产者线程 --》生产者线程的入口函数
//      消费线程   --》消费线程的入口函数

//首先就是需要封装一个线程安全的队列
class BlockQueue
{
    public:
    	//构造
        BlockQueue()
        {
            capacity_ = 10;
            //初始化互斥锁和初始化条件变量
            pthread_mutex_init(&lock_, NULL);
            pthread_cond_init(&prod_cond_, NULL);
            pthread_cond_init(&cons_cond_, NULL);
        }
		
		//析构
        ~BlockQueue()
        {
            pthread_mutex_destroy(&lock_);
            pthread_cond_destroy(&cons_cond_);
            pthread_cond_destroy(&prod_cond_);
        }

		//要能往队列中去插入数据,也能从队列中去获取数据

        //写多线程代码的时候
        //   1.考虑业务核心逻辑
        //   2.考虑核心逻辑当中是否访问临界资源或者说执行临界区代码, 如果有需要保证互斥
        //   3.需不需要保证各个线程之间同步
        //生产者线程调用
        void Push(int data)
        {
            pthread_mutex_lock(&lock_);
            while(que_.size() >= capacity_)
            {
            	//超出了就不要再去往里面放东西了,不是扩不扩容的问题,就还是别忘内部放东西会合适一些
            	//不生产了,那么我就需要进行等待,等待的话,就需要把你放在生产者的条件变量里面
            	//然后传入互斥锁,因为在内部逻辑里面要进行解锁的操作
                pthread_cond_wait(&prod_cond_, &lock_);
            }
			//下面的代码是临界资源的代码,既然要操作临界资源的话,就需要给出锁才可以
            que_.push(data);
            pthread_mutex_unlock(&lock_);

			//解锁完成之后我们就要去通知消费线程了
            pthread_cond_signal(&cons_cond_);
        }

        //消费者线程调用的
        //pop的参数这里我们考虑给他写一个出参
        void Pop(int* data)
        {
            pthread_mutex_lock(&lock_);
            while(que_.empty())
            {
            	//没有东西让你来消费了
                pthread_cond_wait(&cons_cond_, &lock_);
            }
            //要拿数据的话,就去拿队首的数据,因为对首的数据是最先进入到队列里面的
            *data = que_.front();//拿队列首部元素的值
            que_.pop();//出队操作
            pthread_mutex_unlock(&lock_);

			//通知生产者
            pthread_cond_signal(&prod_cond_);
        }
        
    //下面看看有哪些成员变量
    private:
        //STL当中的queue是不安全的
        //STL中的队列并不是线程安全的
        std::queue<int> que_;
        //队列设置一个容量, 不能无节制的让队列一直进行扩容, 有可能会导致我们申请内存失败或者当前程序被操作系统干掉
        //内存不能给成是无限大的,我们需要确保他的容量问题
        size_t capacity_;

        //保证STL当中的queue同步和互斥
        pthread_mutex_t lock_;
        //生产者需要一个条件变量
        //消费者也需要一个条件变量
        pthread_cond_t prod_cond_;
        pthread_cond_t cons_cond_;
};

void* ConsumeStart(void* arg)
{
	//需要用到类去实例化出来对象
    BlockQueue* bq = (BlockQueue*)arg;
    while(1)
    {
    	//对于消费线程他永远去干的事情就是去消费
        //从线程安全队列当中获取数据进行消费
        int data;
        bq->Pop(&data);
        printf("i am %p, i consume %d\n", pthread_self(), data);
    }
    return NULL;
}

void* ProductStart(void* arg)
{
    BlockQueue* bq = (BlockQueue*)arg;
    //如果是多个线程, 每个线程都会在自己独有的栈当中压栈该入口函数, data为临时变量, 每一个线程都是拥有一个data这样的临时
    int data = 0;
    while(1)
    {
        //往线程安全队列当中插入数据
        bq->Push(data);
        //调用完毕Push之后, 互斥锁就释放了
        //这会有可能该执行流时间片到了, 线程切换出去了
        //printf并不是原子性的
        printf("i am %p, i product %d\n", pthread_self(), data);
        data++;
    }
    return NULL;
}

int main()
{
    pthread_t cons[THREADCOUNT], prod[THREADCOUNT];

    BlockQueue* bq = new BlockQueue();

    for(int i = 0;  i < THREADCOUNT; i++)
    {
        int ret = pthread_create(&cons[i], NULL, ConsumeStart, (void*)bq);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }

        ret = pthread_create(&prod[i], NULL, ProductStart, (void*)bq);
        if(ret < 0)
        {
            perror("pthread_create");
            return -1;
        }
    }

    for(int i = 0; i < THREADCOUNT; i++)
    {
        pthread_join(cons[i], NULL);
        pthread_join(prod[i], NULL);
    }
    return 0;
}
  • 程序运行结果
  • 但是需要注意的是,有可能真正运行的结果有可能不是像下面这样子去执行的,她有可能是生产一个消费一个,并不是像这样全部生产完成然会在全部消费完,这取决于操作系统的调度以及时间片等等很多的问题去决定的
    在这里插入图片描述
    在这里插入图片描述
posix版本的信号量
信号量
  • 信号量的本质:计数器+PCB等待队列+一堆的接口(接口中有两个尤为重要的接口,一个是等待接口,一个是唤醒接口);计数器的本质是对资源的计数,当执行流获取信号量成功之后,信号量当中的计数器会进行减一的操作(当获取失败之后,该执行流就会被放到PCB等待队列当中);当执行流释放信号成功之后,信号量当中的计数器会进行加一的操作
  • 信号要完成的任务是同步+互斥
接口
  • 这个接口中有三个参数
  • sem:指的是传入信号量的地址,sem_t 是信号量的类型
  • pshared:该信号量是用于线程之间还是说是用于进程之间,如果值为0的话,是用于线程之间的(其实也就是全局变量),如果值为非0的话,那么就是用于进程之间的(他会将信号量所用到的资源在共享内存当中进行开辟)
  • value:指的是资源的个数,本质上是初始化信号量计数器的,也就是说计数器到底有多少资源是在初始化的时候程序员自己指定的,自己说了算的
    在这里插入图片描述
信号量可以完成线程和线程之间的同步和互斥也可以完成进程和进程之间的同步和互斥
那么如何保证多个线程之间的互斥呢?
  • 互斥,首先,第一步我们需要去初始化信号量当中的计数器为1,表示说只有一个资源可以被使用;然后当执行流A想要访问临界资源的时候,首先去获取信号量,由于计数器当前的值为1,表示的是可以去访问这个临界资源,然后计数器的值从1变为0,从而执行流A去访问临界资源;这个时候,假如说执行流B也想要访问临界资源,那么他首先也是需要去获取信号量,但是计数器当前的值为0,表示不能狗访问到临界资源,执行流B的PCB就被放到了PCB等到队列当中,同时信号量当中的计数器的值会进行减一的操作,然后信号量的值从0变成了-1,表示当前还有1个执行流等待着去访问临界资源
那么,什么是同步呢?
  • 同步:不要求信号量当中计数器的值一定为1,而可以为其他的正整数;当执行流想要去访问临界资源的时候,首先就是要去获取信号量,如果信号量当中的计数器大于0的话,表示能够访问到临界资源的,那么这个执行流就不会阻塞,就会顺序去执行临界区代码;如果信号量当中计数器的值小于等于0,则表示不能访问到临界资源,则该执行流就会被放到Pcb等待队列里面去,同时计数器的值也会进行减一的操作
  • 需要注意的是,如果计数器中的值为负数的话,那么表示当前还有计数器绝对值个执行流在等待
  • 当我们在释放信号量的时候,会对信号量当中计数器的值进行加一的操作,那么这个时候是否需要去唤醒Pcb等待队队列当中的执行流呢?这个时候,是需要分情况去讨论的,当计数器加一操作之后,如果他的值还是负数或者0的话,那么就需要去通知Pcb等待队列当中的执行流了;如果计数器加一操作之后为正数,那么就不需要去通知Pcb等待队列了
等待接口
  • 注意:调用该接口的执行流会对我们的计数器进行减一的操作,如果说,减一操作完毕之后计数器的值是大于等于0的,表示是可以访问临界资源的,这也就意味着sem_wait函数是会返回的;如果说,减一操作完毕之后计数器的值是小于0的,那么调用该接口的执行流就会被阻塞,然后改执行流就会被放到Pcb等待队列里面去
    在这里插入图片描述
释放信号量接口
  • 注意:调用该接口的执行流会对计数器进行加一的操作
    在这里插入图片描述
销毁信号量的接口
  • int sem_destroy(sem_t *sem);
信号量版本的生产者和消费者模型
  • 就是说,需要用到信号量去实现同步和互斥的操作
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <vector>
#include <iostream>

//线程安全的队列
//   只要满足先进先出特性的数据结构都可以称之为队列
//   数组是否可以实现一个队列---是可以的
//   1.读写下标的计算: pos = (pos+1) % 数组大小
//   2.对读写数组实现线程安全的时候
//      互斥:sem_t lock; sem_init(&lock, 0, 1);
//      同步:
//          生产者信号量:sem_t prod; sem_init(&prod, 0, 数组大小)
//      由于一开始数组当中没有空间可以读, 则计数器的初始值为0
//          消费者信号量:sem_t cons; sem_init(&cons, 0, 0);
#define CAPACITY 4
#define THREADCOUNT 2  
//如果线程的数量给成1的话
//只创建一个的意思就是现在只有一个生产者,只有一个消费者

class RingQueue
{
    public:
        RingQueue()
            :vec_(CAPACITY)
        {
            capacity_ = CAPACITY;

            sem_init(&lock_, 0, 1);

			//目前数组当中元素都是没有有效值的,信号量在初始化的时候,可用资源数量就是数组的大小
            sem_init(&prod_, 0, capacity_);

			//读
			//对于读而言,我们现在是不能直接读的,因为数组里面现在其实是没有东西的
			//所以对信号当中的计数器初始化成0
            sem_init(&cons_, 0, 0);

            pos_write_ = 0;
            pos_read_ = 0;
        }

        ~RingQueue()
        {
            sem_destroy(&lock_);
            sem_destroy(&prod_);
            sem_destroy(&cons_);
        }


		//然后我们呢还需要提供两个接口,一个是push接口,一个是pop接口
		//push是要往数组中去写,写的内容是我所需要传递的参数的内容
        void Push(int data)
        {
			//1和2是不可以互相更换位置的,如果1和2互相更换位置了的话
			//那么假设他拿着锁进入了pcb队列的话,另一个人现在想去拿锁的话,他其实是拿不到的,他既然拿不到锁
			//那么他也就没办法去通知你了
            sem_wait(&prod_); //1

            sem_wait(&lock_);  //2
            vec_[pos_write_] = data;
            //需要去更新读写位置
            pos_write_ = (pos_write_ + 1) % capacity_;
            sem_post(&lock_);

            sem_post(&cons_);
        }


		//pop是要进行读的操作,所要读的内容是
		//int*的意思是整形数据的地址
		//data其实是一个出参
        void Pop(int* data)
        {

            sem_wait(&cons_); //1

            sem_wait(&lock_); //2
            *data = vec_[pos_read_];
            pos_read_ = (pos_read_ + 1) % capacity_;
            sem_post(&lock_);

            sem_post(&prod_);
        }
    private:
        std::vector<int> vec_; 
        int capacity_;

		//保证互斥的信号量
        sem_t lock_;
        //保证同步的信号量
        sem_t prod_;
        sem_t cons_;

        //读写位置
        int pos_write_;
        int pos_read_;
};


void* ReadStart(void *arg)
{
	//然后我们现在要去调用队列,既然要调用队列的话,那么肯定需要把队列传递进来
	RingQueue *rq=(RingQueue*) arg;
	//永远都是去进行读的操作
	while(1)
	{
		int data;
		rq->Pop(&data);
		prinf("i am %p,i consume %d\n",pthread_self(),data);
	}
	return NULL;
}

void* WriteStart(void *arg)
{
	RingQueue *rq=(RingQueue*) arg;
	int data=0;
	//永远进行写的操作
	while(1)
	{
		rq->Push(data);
		prinf("i am %p,i product %d\n",pthread_self(),data);
		data++;
	}
	
	return NULL;
}

int main()
{
	RingQueue *rq=new RingQueue();
	pthread_t cons[THREADCOUNT],prod[THREADCOUNT];
	for(int i=0;i<THREADCOUNT;i++)
	{
		int ret=pthread_create(&cons[i],NULL,ReadStart,(void*)rq);
		if(ret<0)
		{
			perror("pthread_create");
			return -1;
		}
		ret=pthread_create(&prod[i],NULL,WriteStart,(void*)rq);
		if(ret<0)
		{
			perror("pthread_create");
			return -1;
		}
	}
	
	//防止内存泄漏问题的发生
	for(int i=0;i<THREADCOUNT;i++)
	{
		pthread_join(cons[i],NULL);
		pthread_join(prod[i],NULL);
	}
	
	//释放掉之前所申请的内存空间
	delete rq;
	rq=NULL;
    return 0;
}
  • 代码运行结果:
    在这里插入图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值