线程间同步详解

同步和通信的区别

线程间同步用于控制多个线程按照一定的顺序访问资源。

线程间通信用于进程间传输信息,线程同步是一种进程通信的表现形式,通过修改信号量,线程之间可相互协调运行和协同工作。

线程间同步机制

线程间同步有互斥量、信号量、管程等几种方式,在介绍线程间同步之前,首先要介绍一下临界区的概念:临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段。在任意时刻只允许一个线程对临界区进行访问,如果有多个线程试图访问临界区,那么在有一个线程进入后,其他试图访问临界区的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。临界区只能用于在同一进程里线程间的互斥访问

互斥量

只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥量比临界区复杂,互斥量不仅仅能够在同一进程的不同线程间实现资源的安全共享,而且可以在不同进程的线程间实现对资源的安全共享

信号量

信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,但是需要限制在同一时刻访问此资源的最大线程数目。这与操作系统中的PV操作相同。

  • P操作:使信号量减1,若信号量大于等于0,则该进程继续执行,否则排入等待队列。
  • V操作:使信号量加1,若信号量大于0 ,唤醒等待队列中的一个进程。

锁(互斥量)和信号量的区别

  1. 互斥量用于线程的互斥,互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性,但互斥无法限制访问者对资源的访问顺序,即访问是无序的信号量用于线程的同步,大多数情况下,同步是指在互斥的基础上通过其它机制实现访问者对资源的有序访问,少数情况下同步是指可以允许多个访问者同时访问资源。
  2. 互斥量值只能为0/1,信号量值可以为非负整数。一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
  3. 加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

三个经典的进程间同步问题(生产者消费者问题、读者写者问题、哲学家就餐问题)

生产者消费者问题

我们可把共享缓冲区中的n个缓冲块视为共享资源,生产者写入数据的缓冲块成为消费者可用资源,而消费者读出数据后的缓冲块成为生产者的可用资源。为此,可设置三个信号量:full、empty和mutex。其中:full表示有数据的缓冲块数目,初值是0;empty表示空的缓冲块数初值是n;mutex用于访问缓冲区时的互斥,初值是1。实际上,full和empty间存在如下关系:full + empty = N

注意:这里每个进程中各个P操作的次序是重要的。各进程必须先检查自己对应的资源数在确信有可用资源后再申请对整个缓冲区的互斥操作;否则,先申请对整个缓冲区的互斥操后申请自己对应的缓冲块资源,就可能死锁。
在这里插入图片描述

读者写者问题

读者写者问题要求如下:

  • 允许多个读者可以同时对文件执行读操作。
  • 只允许一个写者往文件中写信息。
  • 任一写者在完成写操作之前不允许其他读者或写者工作。
  • 写者执行写操作前,应让已有的读者和写者全部退出。

(1)基本格式
在这里插入图片描述

这种写法读进程与读进程之间也变成了必须互斥访问共享数据,并不满足题目读进程与读进程可以同时访问共享数据的要求。

(2)引入count变量,用来记录当前有几个读进程在访问共享数据
在这里插入图片描述

如果读进程A想要访问共享数据,并且执行了P(rw)“上锁”操作,此时,此时,读进程B也想要访问共享数据,且count==0,因此又会执行P(rw),但是因为进程A已经执行了“上锁”操作,所以进程B还是会被阻塞,无法访问共享数据。可见,仍然没有达到多个读进程可以同时访问共享数据的目的!

出现这种问题的原因:
对于count变量的检查与赋值操作无法“一气呵成”,可以被中断。

(3)引入mutex,防止count++操作被中断
在这里插入图片描述

增加一个mutex互斥信号量来保证if判断语句 和 count++(count–) 能够“一气呵成”执行完,中间不会被打断,保证各进程对count的访问是互斥的。

但是,只要又读进程在读取共享数据,写进程就要一直阻塞等待,这很可能导致写进程一直无法往共享数据中写入数据,也就是说写进程很有可能会被“饿死”。

(4)设置变量semaphore w =1 ,用于实现“读写公平”
在这里插入图片描述

在这里插入图片描述

经过上面的“改造”,我们来验证一下是否达到了“写优先”的目的:
假设有三个进程先后到来:读进程A——>写进程a——>读进程B

假设读进程A正在访问共享数据,那么读进程A肯定已经执行了P(w)、P(mutex)、P(rw)、V(mutex)、V(w)。此时,写进程a也想要访问共享数据,那么当读进程a执行P(w)时,不会被阻塞,但是执行到P(rw)时,由于读进程A还没有执行V(rw)“解锁”操作,所以,写进程a会被阻塞等待。

而如果此时有第二个读进程B也想要访问共享数据,但由于之前第一个读进程A已经执行了P(w)“上锁”操作,所以当读进程B执行到P(w)操作时,也会被堵塞等待。

直到读进程A完成了读文件操作后,执行了V(rw)“解锁”操作,写进程a才会被“唤醒”。然后在写进程完成了写文件操作后,执行了V(w)“解锁”操作,读进程B才能被唤醒。

注意:这里为什么会先唤醒写进程a呢?
答:因为这里是写进程a比读进程B先想要访问共享数据,所以优先被唤醒。这里其实就是“先来先服务算法”。

哲学家就餐问题

因为是五位哲学家,并且每位哲学家的各自做自己的事情(思考和吃饭),因此可以创建五个线程表示五位哲学家,五个线程相互独立(异步)。并对五位哲学家分别编号为0~4。

同时,有五根筷子,每根筷子只对其相邻的两位哲学家是共享的,因此这五根筷子可以看做是五种不同的临界资源(不是一种资源有5个,因为每根筷子只能被固定编号的哲学家使用)。并对五根筷子分别编号为0~4,其中第i号哲学家左边的筷子编号为i,则其右边的筷子编号就应该为(i + 1) % 5。

因为筷子是临界资源,因此当一个线程在使用某根筷子的时候,应该给这根筷子加锁,使其不能被其他进程使用。

根据以上分析,可以使用pthread_create函数创建五个线程,可以使用pthread_mutex_t chops[5]表示有五根筷子,五个不同的临界资源,并用pthread_mutex_init(&chops[i], NULL);来初始化他们。
在这里插入图片描述

每位哲学家先思考,当某位哲学家饥饿的时候,就拿起他左边的筷子,然后拿起他右边的筷子,然后进餐,然后放下他左右的筷子并进行思考。这里面有一个很严重的问题,就是死锁,假设一开始每位哲学家都拿起其左边的筷子,然后每位哲学家又都尝试去拿起其右边的筷子,这个时候由于每根筷子都已经被占用,因此每位哲学家都不能拿起其右边的筷子,只能等待筷子被其他哲学家释放。由此五个线程都等待被其他进程唤醒,因此就陷入了死锁。

解决死锁的方法有如下几种:
(1)信号量
第一种解决死锁问题的办法就是同时只允许四位哲学家同时拿起同一边的筷子,这样就能保证一定会有一位哲学家能够拿起两根筷子完成进食并释放资源,供其他哲学家使用,从而实现永动,避免了死锁。举个最简单的栗子,假定0~3号哲学家已经拿起了他左边的筷子,然后当4号哲学家企图去拿他左边的筷子的时候,将该哲学家的线程锁住,使其拿不到其左边的筷子,然后其左边的筷子就可以被3号哲学家拿到,然后3号哲学家进餐,释放筷子,然后更多的哲学家拿到筷子并进餐。

如何才能实现当4号哲学家企图拿起其左边的筷子的时候将该哲学家的线程阻塞?这个时候就要用到信号量机制。因为同时只允许有四位哲学家同时拿起左筷子,因此我们可以设置一个信号量r,使其初始值为4,然后每当一位哲学家企图去拿起他左边的筷子的时候,先对信号量做一次P操作,从而当第五位哲学家企图去拿做筷子的时候,对r做一次P操作,r = -1,由r < 0得第五位哲学家的线程被阻塞,从而不能拿起左筷子,因此也就避免了死锁问题。然后当哲学家放下他左边的筷子的时候,就对r做一次V操作。代码如下:
在这里插入图片描述

(2)分奇偶
规定奇数号哲学家先拿起他左边的筷子,然后再去拿他右边的筷子,而偶数号的哲学家则相反,这样的话总能保证一个哲学家能获得两根筷子完成进餐,从而释放其所占用的资源,代码如下:

在这里插入图片描述

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值