Linux 线程如何实现同步与互斥_在unix中线程应该通过什么机制实现互斥或进步

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

给大家整理的电子书资料:

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

在这里插入图片描述
我们发现可以安全得进行抢票,并不会出现抢到负数的票了

我们发现所有的线程都是通过同一个互斥量来实现互斥的,也就意味着这个互斥变量也是一个临界资源,大家都可以访问得到,所以互斥变量本身都不是安全的,那这样子如何保证别的线程的安全呢?其实设计互斥变量时就将它自身的计数器置为原子性操作

如果mutex是一个普通变量的情况,操作系统首先会将mutex的值加载到寄存器中,再判断寄存器中的值是否为1,如果为1就加锁,并将0写入到mutex中,然后访问临界资源,如果是0则让线程等到。但是在这个加锁并将0写到mutex中这个操作并非原子操作,如果时间片到了,mutex的值还是1,这时候会有其他线程进来加锁访问,会导致线程的不安全。
而情况并非这样,操作系统知道你是一个互斥变量,会先将寄存器的值置为0,然后直接将寄存器的值与内存中的mutex数据进行交换,这个交换操作是原子性操作(单条指令),这时候mutex的值肯定是0了,当有线程访问时肯定会是等待的了,然后再根据寄存器中的值进行判断是否可以加锁。加锁后再解锁,将mutex置回来

同步的实现

我们再来看看黄牛抢票的代码,如果我们打印线程的tid,也就是看哪个线程抢到的票

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

//票数
int ticket = 100;
//1、定义e互斥变量
pthread_mutex_t mutex;

//线程函数
void \*thr\_scalpers(void \*arg)
{
    while (1)
    {
    	//3、加锁一定是只加锁临界资源的访问
        pthread\_mutex\_lock(&mutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%p-i got a ticket:%d\n",pthread\_self(), ticket);
            --ticket;
            //4、解锁
            pthread\_mutex\_unlock(&mutex);
        }
        else
        {
            //加锁后在任意有可能退出线程的地方都要解锁
            pthread\_mutex\_unlock(&mutex);
            pthread\_exit(NULL);
        }
    }
    return NULL;
}

int main()
{
    //4个线程
    pthread_t tid[4];

    int i, ret;
    //2、互斥变量的初始化一定要放在线程创建之前
    pthread\_mutex\_init(&mutex, NULL);
    for (i = 0; i < 4; ++i)
    {
        //创建4个线程并执行
        ret = pthread\_create(&tid[i], NULL, thr_scalpers, NULL);
        if (ret != 0)
        {
            printf("thread create error");
            return -1;
        }
    }

    for (i = 0; i < 4; ++i)
    {
        pthread\_join(tid[i], NULL);
    }
    //5、一定要在所以线程不使用互斥变量后在销毁
    pthread\_mutex\_destroy(&mutex);

    return 0;
}

在这里插入图片描述
我们发现都是一个线程在执行,也就是说互斥并不保证资源分配的合理性,只保证了资源分配的安全性

条件变量

条件变量实现同步原理:是去判断当前线程是否满足获取资源的条件,当线程获取条件不满足的时候,调用阻塞接口,使线程阻塞,将pcb挂到等待队列上。等到条件满足的时候通过唤醒接口唤醒等待队列上的阻塞了的线程

条件变量提供了一个pcb等待队列,以及使线程阻塞的接口和唤醒线程的接口

条件的判断是用户进行的操作,判断线程是否满足条件,不满足的时候调用条件变量接口使线程等待

操作流程

  1. 定义条件变量 pthread_cond_t cond
  2. 初始化条件变量 pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr)
    定义并初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER
  3. 获取资源条件,不满足时挂起休眠pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)条件变量时搭配互斥变量一起使用的(原因是条件变量本事也是一个临界资源,需要被保护)
    int pthread_cond_timedwait(pthread_cond_t *cond, 一直等待添加满足后唤醒pthread_mutex_t *mutex, const struct timespec *abstime)这个接口是可以设置阻塞超时后可以自行唤醒
  4. 唤醒线程 pthread_cond_signal(pthread_cond_t *cond)唤醒至少一个线程; pthread_cond_broadcast(pthread_cond_t *cond)唤醒所有等待的线程
  5. 销毁条件变量 pthread_cond_destroy(pthread_cond_t *cond)

我们拿顾客和厨师来模拟实现同步的过程(保证资源利用合理性)
顾客(线程A) 厨师(线程B) 碗(资源)
顾客操作:
1、预定这个碗(加锁)
2、看碗是否有饭,没有饭就等着,把碗给厨师(访问资源前先判断是否满足访问条件,解锁,不满足就阻塞等待)
3、碗有饭,吃饭(加锁,满足访问条件,利用并占有该资源)
4、吃完后,奖励自己再来一碗(释放资源,唤醒另一个线程)
5、把碗还给厨师(解锁)

厨师操作:
1、把饭放到指定碗中(加锁)
2、看碗是否有饭,有饭就等着,把碗给顾客(访问资源前先判断是否满足访问条件,解锁,不满足就阻塞等待)
3、碗没有饭,做饭(加锁,满足访问条件,利用并占有该资源)
4、做完饭后叫顾客吃饭(释放资源,唤醒另一个线程)
5、把碗给客户(解锁)

代码实现

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

//默认0表示碗中没有饭
int bowl = 0;

//实现线程间对bowl变量访问的同步操作
pthread_cond_t cond;
//保护bowl变量的访问操作
pthread_mutex_t mutex;

void \*thr\_cook(void \*arg)
{
    while (1)
     {
        //加锁
        pthread\_mutex\_lock(&mutex);
        if (bowl != 0)//表示有饭,不满足做饭条件
        {
            //解锁,并让厨师线程等待,被唤醒后再加锁
            //在该接口中解锁和休眠操作是一步完成的,保证操作的原子性
            pthread\_cond\_wait(&cond, &mutex);//3步操作一个接口完成

        }
        bowl = 1;//能够走下来表示没饭,做完后置为1
        printf("i made a bowl of rice\n");
        //唤醒顾客吃饭
        pthread\_cond\_signal(&cond);
        //解锁
        pthread\_mutex\_unlock(&mutex);
    }
    return NULL;
}
void \*thr\_customer(void \*arg)
{
    while (1)
    {
         //加锁
        pthread\_mutex\_lock(&mutex);
        if (bowl != 1)//没有饭,不满足吃饭条件
        {
            //没有饭,先解锁,后等待
            pthread\_cond\_wait(&cond, &mutex);
        }
        bowl = 0;//能够走下来表示有饭,吃完后置为0
        printf("i had a bowl of rice, it was delicious\n");
        //唤醒厨师做饭
        pthread\_cond\_signal(&cond);
        //解锁
        pthread\_mutex\_unlock(&mutex);
    }
    return NULL;
}

int main()
{
    //厨师线程
    pthread_t cook_tid;
    //顾客线程
    pthread_t customer_tid;

    int ret;
    pthread\_mutex\_init(&mutex, NULL);
    pthread\_cond\_init(&cond, NULL);
    ret = pthread\_create(&cook_tid, NULL, thr_cook, NULL);
    if (ret != 0)
    {
        printf("pthread\_create error\n");
        return -1;
    }
    ret = pthread\_create(&customer_tid, NULL, thr_customer, NULL);
    if (ret != 0)
    {
        printf("pthread\_create error\n");
        return -1;
    }
	
	//等待厨师和顾客线程
    pthread\_join(cook_tid, NULL);
    pthread\_join(customer_tid, NULL);       	
    pthread\_mutex\_destroy(&mutex);
    pthread\_cond\_destroy(&cond);
    return 0;
}

运行结果:我们发现通过同步与互斥,就可以实现对临界资源访问的安全性和合理性,不会出现没饭也吃的情况
在这里插入图片描述
但是如果存在多个顾客和多个厨师,会发生什么情况呢?
只提供修改主函数中的代码,其他代码和上面一样

int main()
{
    //厨师线程
    pthread_t cook_tid[4];
    //顾客线程
    pthread_t customer_tid[4];

    int ret, i;
    pthread\_mutex\_init(&mutex, NULL);
    pthread\_cond\_init(&cond, NULL);
    for (i = 0; i < 4; ++i){
        ret = pthread\_create(&cook_tid[i], NULL, thr_cook, NULL);
        if (ret != 0)
        {
            printf("pthread\_create error\n");
            return -1;
        }
    }
    for (i = 0; i < 4; ++i){
        ret = pthread\_create(&customer_tid[i], NULL, thr_customer, NULL);
        if (ret != 0)
        {
            printf("pthread\_create error\n");
            return -1;
        }
    }
    pthread\_join(cook_tid[0], NULL);
    pthread\_join(customer_tid[0], NULL);
    pthread\_mutex\_destroy(&mutex);
    pthread\_cond\_destroy(&cond);
    return 0;
}

运行结果:
在这里插入图片描述
我们发现,当存在多个顾客和多个厨师时,出现了没饭也吃,有饭也做的情况,这肯定不是我们想要的,那原因处在哪呢?

一开始有多个顾客线程,因为没有饭都进行等待,当一个厨师做好饭后,因为调用的是pthread_cond_signal接口唤醒顾客,唤醒了至少1个顾客,也就是多个顾客,当有一个顾客加锁成功后,就去吃饭,剩下的顾客线程只能卡在加锁这里。加锁成功吃完饭的顾客线程去唤醒厨师并进行解锁,由于cpu是时间片调用线程的,加锁的不一定时厨师,有可能是卡在加锁那块的顾客线程,当顾客线程加锁后就去吃饭,但是此刻是没有饭的。所以就发生了错误如何避免这个错误呢?在第二步的条件判断应该改为循环判断,这样即使顾客加锁成功被唤醒后发现没饭也不会去吃了,而是看大没饭就继续休眠,有饭再吃

只提供修改两个线程入口函数的代码,其他代码和上面一样

void \*thr\_cook(void \*arg)
{
    while (1)
     {
        //加锁
        pthread\_mutex\_lock(&mutex);
        while (bowl != 0)//表示有饭,不满足做饭条件
        {
            //解锁,并让厨师线程等待,被唤醒后再加锁
            //在该接口中解锁和休眠操作是一步完成的,保证操作的原子性
            pthread\_cond\_wait(&cond, &mutex);//3步操作一个接口完成

        }
        bowl = 1;//能够走下来表示没饭,做完后置为1
        printf("i made a bowl of rice\n");
        //唤醒顾客吃饭
        pthread\_cond\_signal(&cond);
        //解锁
        pthread\_mutex\_unlock(&mutex);
    }
    return NULL;
}
void \*thr\_customer(void \*arg)
{
    while (1)
    {
         //加锁
        pthread\_mutex\_lock(&mutex);
        while (bowl != 1)//没有饭,不满足吃饭条件
        {
            //没有饭,先解锁,后等待
            pthread\_cond\_wait(&cond, &mutex);
        }
        bowl = 0;//能够走下来表示有饭,吃完后置为0
        printf("i had a bowl of rice, it was delicious\n");
        //唤醒厨师做饭
        pthread\_cond\_signal(&cond);
        //解锁
        pthread\_mutex\_unlock(&mutex);
    }
    return NULL;
}

运行结果:
在这里插入图片描述
虽然不会出现没饭吃饭,有饭做饭的情况,但是我们发现程序发生了阻塞,不继续往下执行了,这又是什么原因呢?
条件变量只有一个,意味着等待队列只有一个,顾客没饭吃就挂到等待队列上,厨师不能做饭也要挂到等待队列上,假设一个厨师线程做完饭后,要去唤醒一个顾客线程去吃饭,但是却唤醒的是厨师线程(顾客线程和厨师线程都在同一个队列),唤醒的厨师线程发现有饭,又重新挂载等待队列中,从而导致程序阻塞如何避免这种情况呢?不同角色的线程应该在不同的等待队列上进行等待,这样等唤醒的时候,就不会唤醒同类型的线程了,因此存在多个角色线程,就应该设置多个条件变量(一个条件变量对应一个等待队列)

完整正确代码:

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

//默认0表示碗中没有饭
int bowl = 0;

//实现线程间对bowl变量访问的同步操作
pthread_cond_t cook_cond;
pthread_cond_t customer_cond;
//保护bowl变量的访问操作
pthread_mutex_t mutex;

void \*thr\_cook(void \*arg)
{
    while (1)
     {
        //加锁
        pthread\_mutex\_lock(&mutex);
        while (bowl != 0)//表示有饭,不满足做饭条件
        {
            //解锁,并让厨师线程等待,被唤醒后再加锁
            //在该接口中解锁和休眠操作是一步完成的,保证操作的原子性
            pthread\_cond\_wait(&cook_cond, &mutex);//3步操作一个接口完成

        }
        bowl = 1;//能够走下来表示没饭,做完后置为1
        printf("i made a bowl of rice\n");
        //唤醒顾客吃饭
        pthread\_cond\_signal(&customer_cond);
        //解锁
        pthread\_mutex\_unlock(&mutex);
    }
    return NULL;
}
void \*thr\_customer(void \*arg)
{
    while (1)
    {
         //加锁
        pthread\_mutex\_lock(&mutex);
        while (bowl != 1)//没有饭,不满足吃饭条件
        {
            //没有饭,先解锁,后等待
            pthread\_cond\_wait(&customer_cond, &mutex);
        }
        bowl = 0;//能够走下来表示有饭,吃完后置为0
        printf("i had a bowl of rice, it was delicious\n");
        //唤醒厨师做饭
        pthread\_cond\_signal(&cook_cond);
        //解锁
        pthread\_mutex\_unlock(&mutex);
    }
    return NULL;
}

int main()
{
    //厨师线程
    pthread_t cook_tid[4];
    //顾客线程
    pthread_t customer_tid[4];

    int ret, i;
    pthread\_mutex\_init(&mutex, NULL);
    pthread\_cond\_init(&cook_cond, NULL);
    pthread\_cond\_init(&customer_cond, NULL);
    for (i = 0; i < 4; ++i){
        ret = pthread\_create(&cook_tid[i], NULL, thr_cook, NULL);
        if (ret != 0)
        {
            printf("pthread\_create error\n");
            return -1;
        }
    }
    for (i = 0; i < 4; ++i){
        ret = pthread\_create(&customer_tid[i], NULL, thr_customer, NULL);
        if (ret != 0)
        {
            printf("pthread\_create error\n");
            return -1;
        }
    }
    pthread\_join(cook_tid[0], NULL);
    pthread\_join(customer_tid[0], NULL);
    pthread\_mutex\_destroy(&mutex);
    pthread\_cond\_destroy(&cook_cond);
    pthread\_cond\_destroy(&customer_cond);
    return 0;
}

运行结果:
在这里插入图片描述
使用注意事项总结:
1、条件变量需要搭配互斥锁一起使用
2、在每一个有可能退出线程的地方都需要解锁
3、条件变量使用时对条件的判断应该使用while循环来判断
4、多种角色线程应该使用多个条件变量

练习:生产者与消费者模型

信号量

信号量可以用于实现线程或者进程同步与互斥(主要用于同步)
信号量 = 一个计数器 + pcb等待队列
同步原理:通过自身计数器对资源进行计数,并通过计数器的资源计数,判断进程/线程是否能够符合访问资源的条件,若符合就可以访问,若不符合则调用提供的接口让进程/线程加入到pcb等待队列中;其他进程/线程促使条件满足之后,可以唤醒pcb等待队列上的进程/线程

为了做好运维面试路上的助攻手,特整理了上百道 【运维技术栈面试题集锦】 ,让你面试不慌心不跳,高薪offer怀里抱!

这次整理的面试题,小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。

本份面试集锦涵盖了

  • 174 道运维工程师面试题
  • 128道k8s面试题
  • 108道shell脚本面试题
  • 200道Linux面试题
  • 51道docker面试题
  • 35道Jenkis面试题
  • 78道MongoDB面试题
  • 17道ansible面试题
  • 60道dubbo面试题
  • 53道kafka面试
  • 18道mysql面试题
  • 40道nginx面试题
  • 77道redis面试题
  • 28道zookeeper

总计 1000+ 道面试题, 内容 又全含金量又高

  • 174道运维工程师面试题

1、什么是运维?

2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

3、现在给你三百台服务器,你怎么对他们进行管理?

4、简述raid0 raid1raid5二种工作模式的工作原理及特点

5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

7、Tomcat和Resin有什么区别,工作中你怎么选择?

8、什么是中间件?什么是jdk?

9、讲述一下Tomcat8005、8009、8080三个端口的含义?

10、什么叫CDN?

11、什么叫网站灰度发布?

12、简述DNS进行域名解析的过程?

13、RabbitMQ是什么东西?

14、讲一下Keepalived的工作原理?

15、讲述一下LVS三种模式的工作过程?

16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

17、如何重置mysql root密码?

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

7、Tomcat和Resin有什么区别,工作中你怎么选择?

8、什么是中间件?什么是jdk?

9、讲述一下Tomcat8005、8009、8080三个端口的含义?

10、什么叫CDN?

11、什么叫网站灰度发布?

12、简述DNS进行域名解析的过程?

13、RabbitMQ是什么东西?

14、讲一下Keepalived的工作原理?

15、讲述一下LVS三种模式的工作过程?

16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

17、如何重置mysql root密码?

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值