一文搞懂信号量机制(内含消费者生产者问题)

目录

1.信号量机制

信号量机制概念和理解

信号量

整形信号量

记录型信号量

2.信号量机制实现互斥和同步

信号量机制实现互斥

信号量机制实现同步

3.生产者消费者问题

4.多生产者多消费者问题

5.吸烟者问题

6.读者写者问题

 读者优先算法:

 写者优先算法

          读写公平算法

7.哲学家问题

8.总结


1.信号量机制

信号量机制概念和理解

信号量机制是一种用于控制多个并发进程或线程访问共享资源的同步机制。它通过使用一个或多个计数器来实现。

每个信号量都有一个初始值,可以用于控制并发访问的数量。当一个进程或线程想要访问共享资源时,它必须首先获取信号量,如果信号量的计数器大于零,则减少计数器的值并允许访问共享资源。如果计数器的值为零,则进程或线程将被阻塞,直到有另一个进程或线程释放信号量并增加计数器的值。

一旦进程或线程完成了对共享资源的访问,它必须释放信号量并增加计数器的值,以便其他进程或线程可以获得访问权限。

信号量机制可用于避免死锁,控制资源的使用和访问,以及实现进程同步。在操作系统和并发编程中,信号量被广泛使用。


如果你没明白上面的意思,那么我举一个例子让你尽可能的理解这个问题:

假设你和你的朋友们想要一起做一道菜,但是只有一张烤箱可以使用。这时候,你们需要协调谁来使用烤箱,以免发生冲突和浪费时间。

信号量机制就像是一个小助手,它会帮你们控制使用烤箱的顺序。每个人需要先向小助手申请使用权,小助手会检查烤箱是否被占用,如果没有,小助手就会把烤箱借给这个人使用,同时减少烤箱计数器的值。如果烤箱正在被占用,小助手会让这个人等待,直到有其他人释放了烤箱,然后小助手才会把烤箱借给他。

当一个人烤完菜之后,他需要向小助手归还烤箱,并增加计数器的值,这样其他人就可以借到烤箱了。

信号量机制就是这样一个小助手,它可以帮助多个进程或线程协调对共享资源的使用,避免互相冲突和浪费时间。


信号量

信号量(Semaphore)是一种变量(可以是简单的一个整数,也可以是复杂的结构体)。信号量可以看作是一个计数器,用于跟踪可用资源的数量。


整形信号量

整型信号量(Integer Semaphore)是一种信号量的实现方式,它是一个整数变量,用于表示可用资源的数量。整型信号量可以是正数、零或负数(这种情况极少,主要理解非负数就可以),因此它可以用于解决各种不同的进程同步问题。

整型信号量具有两个基本操作:P操作和V操作。P操作(也称为“申请”或“等待”操作)用于尝试获取资源,V操作(也称为“释放”或“信号”操作)用于释放资源。

当一个进程要使用一个共享资源时,它会尝试执行P操作,如果整型信号量的值大于0,则表示有可用的资源,进程可以获取该资源并将整型信号量的值减1。如果整型信号量的值等于0,则表示没有可用的资源,进程必须等待,直到有可用资源,并且其他进程释放了整型信号量。

虽然整型信号量的值通常是非负数,但在某些情况下,整型信号量的值可能会是负数。例如,在某些实现中,当多个进程等待获取资源时,信号量的值可能会变成负数,表示有多个进程正在等待获取资源。此时,当资源变得可用时,信号量的值可能会变为正数,以表示有新的资源可用。但这种情况只是在一些特殊的实现中存在,通常并不是整型信号量的常见使用方式。

在某些实现中,当多个进程等待获取资源时,整型信号量的值可能会变成负数,这种情况也称为信号量的“饥饿状态”。在这种情况下,整型信号量的值表示等待获取资源的进程数量减去已释放资源的数量。

例如,假设有一个整型信号量,初始值为1,表示共有1个资源可用。现在有两个进程要使用这个资源,它们同时执行P操作,因此整型信号量的值会减2,变成-1。此时,整型信号量的值为负数,表示有两个进程正在等待获取资源,但是只有1个资源可用,因此它们必须等待其他进程释放资源。

当有一个进程释放资源时,它执行V操作,将整型信号量的值加1,以表示有一个新的资源可用。此时整型信号量的值会变成0,表示有1个资源可用。但是,如果此时有多个进程正在等待获取资源,那么它们中只有一个能够获取该资源,并将整型信号量的值减1,变成-1。这种情况下,整型信号量的值仍然为负数,表示还有一个进程正在等待获取资源,因此其他进程必须等待,直到有新的资源可用。

这种情况下整型信号量的值为负数是可能的,但它通常只是在一些特殊的实现中存在,因此在实际应用中,应该将整型信号量的值定义为非负数。

当一个进程释放一个资源时,它将执行V操作,将整型信号量的值加1,表示有一个新的资源可用。如果有多个进程正在等待获取资源,那么V操作会唤醒其中的一个进程,让它去获取资源。

整型信号量是一种比较简单且常见的信号量实现方式,它可以用于解决各种进程同步问题,如生产者消费者问题、读写锁问题、临界区问题等。但是,整型信号量有一些局限性,比如在多核心、多线程的环境下,可能存在竞争条件和死锁等问题。因此,在使用整型信号量时,需要注意正确使用和合理配置信号量的初始值等。

整型信号量中的P(wait)和V(signal)操作的理解:

一文搞懂信号量机制(内含消费者生产者问题)_第1张图片


记录型信号量

记录型信号量通常由一个整数值和与之相关联的操作组成。具体来说,记录型信号量通常由以下三个部分组成:

  1. 计数器:记录当前可用的资源数目。该计数器通常是一个整数变量,可以表示为一个共享内存地址。

  2. 原子操作:确保对计数器的修改是原子的,即保证多个线程或进程可以安全地并发访问该计数器。原子操作可以使用硬件原语(例如CAS指令)或操作系统提供的原子操作API来实现。

  3. 队列:用于保存被阻塞等待资源的线程或进程的信息。当计数器的值为0时,线程或进程将被加入到队列中,并等待资源变得可用。当资源可用时,队列将被唤醒,并按照某种策略(例如先进先出)选择一个线程或进程以获得该资源。

需要注意的是,具体的记录型信号量实现方式可能会有所不同,但它们通常都包括以上三个部分。此外,记录型信号量还可能包括一些其他的属性或方法,例如锁定和释放锁定等。

为了简化对记录型信号量的理解,我们默认原子操作已经放到了函数之中:

一文搞懂信号量机制(内含消费者生产者问题)_第2张图片

一文搞懂信号量机制(内含消费者生产者问题)_第3张图片 由上面的内容可以看出,记录型信号量很巧妙的避免了忙等,当资源不足的时候直接将申请资源的这个进行由运行态变成阻塞态,这样就可以自己主动放弃cpu的占用权。符合让权等待。


2.信号量机制实现互斥和同步

信号量机制实现互斥

通过上面的学习,相信你一定对信号量机制有了一个很透彻的理解,现在让我们应用到实际问题中吧!

信号量是一种常用的同步机制,用于协调并发进程或线程之间的操作。在信号量机制中,互斥指的是一次只能有一个进程或线程进入临界区域(critical section),从而避免并发访问的冲突。下面是使用信号量机制实现互斥的步骤:

  1. 定义一个初始值为1的二元信号量(binary semaphore),用于保护共享资源。

  2. 在进入临界区之前,使用P操作(也称为wait操作)请求信号量,如果信号量的值为1,则可以进入临界区。如果信号量的值为0,则该进程或线程会被阻塞,直到信号量的值变为1。

  3. 进入临界区,执行需要保护的操作。

  4. 在离开临界区之前,使用V操作(也称为signal操作)释放信号量,将信号量的值恢复为1,表示共享资源已经释放。

下面是使用伪代码实现互斥的示例:

var mutex = 1; // 定义二元信号量,初始值为1

// 进程或线程 A
P(mutex); // 请求信号量
// 进入临界区,执行需要保护的操作
V(mutex); // 释放信号量

// 进程或线程 B
P(mutex); // 请求信号量,如果此时 mutex 的值为 1,则可以进入临界区;如果 mutex 的值为 0,则会被阻塞
// 进入临界区,执行需要保护的操作
V(mutex); // 释放信号量,将 mutex 的值恢复为 1

 使用信号量机制实现互斥可以避免多个进程或线程同时访问共享资源,从而保证数据的一致性和正确性。但需要注意的是,在使用信号量机制时,应避免死锁等问题的发生。

一文搞懂信号量机制(内含消费者生产者问题)_第4张图片

本图用于辅助理解。


信号量机制实现同步

信号量机制不仅可以实现互斥,还可以实现同步,用于协调多个进程或线程之间的操作顺序。在信号量机制中,同步指的是一些操作必须按照特定的顺序进行,以确保数据的正确性和一致性。下面是使用信号量机制实现同步的步骤:

  1. 定义一个初始值为0的计数型信号量(counting semaphore),用于等待操作的完成。

  2. 在需要等待操作完成的进程或线程中,使用P操作请求信号量,如果信号量的值大于0,则可以继续执行。如果信号量的值为0,则该进程或线程会被阻塞,直到有其他进程或线程使用V操作释放信号量。

  3. 在完成操作的进程或线程中,使用V操作释放信号量,将信号量的值加1。

下面是使用伪代码实现同步的示例:

var semaphore = 0; // 定义计数型信号量,初始值为0

// 进程或线程 A
// 执行操作1
V(semaphore); // 释放信号量,使 semaphore 的值加1

// 进程或线程 B
P(semaphore); // 请求信号量,如果此时 semaphore 的值为 0,则会被阻塞,直到 semaphore 的值变为大于0为止
// 执行操作2

 使用信号量机制实现同步可以保证操作的顺序和正确性,从而避免数据不一致等问题的发生。但需要注意的是,在使用信号量机制时,应避免死锁等问题的发生,并且需要考虑多个进程或线程之间的调度顺序和优先级,以确保程序的正确性和性能。

一文搞懂信号量机制(内含消费者生产者问题)_第5张图片

 一文搞懂信号量机制(内含消费者生产者问题)_第6张图片

通过同步的方式,我们可以解决下面的问题:

在进程管理中,有些进程需要等待其他进程完成特定的任务后才能开始执行,这种关系称为前驱关系。例如,一个进程需要等待其他进程先执行完某些操作,才能开始执行自己的操作。在这种情况下,可以使用信号量机制来实现前驱关系。

具体实现步骤如下:

  1. 对于需要等待其他进程的进程,定义一个计数型信号量(counting semaphore),初始值为0。

  2. 在等待进程中,使用P操作请求信号量。如果计数型信号量的值为0,则该进程会被阻塞,直到其他进程执行完特定任务并使用V操作释放信号量。如果计数型信号量的值大于0,则可以继续执行。

  3. 在其他进程完成特定任务后,使用V操作释放计数型信号量,使计数型信号量的值加1。

一文搞懂信号量机制(内含消费者生产者问题)_第7张图片 在实现前驱关系时,需要考虑多个进程或线程之间的调度顺序和优先级,以确保程序的正确性和性能。同时,还需要注意死锁等问题的发生,并进行适当的处理。


3.生产者消费者问题

生产者消费者问题是一个经典的同步问题,在多进程或多线程的环境下,生产者进程负责生产数据,而消费者进程负责消费数据。这两个进程之间需要通过共享缓冲区来进行通信。生产者进程将数据放入缓冲区,消费者进程从缓冲区取出数据进行消费。在这个过程中,需要保证生产者和消费者之间的同步和互斥,避免竞争条件的发生。

信号量机制是一种经典的解决生产者消费者问题的方法,其基本思路如下:

  1. 定义两个计数型信号量,一个表示缓冲区中空闲的位置数量(初始值为缓冲区大小),一个表示缓冲区中已经存储的数据数量(初始值为0)。

  2. 生产者进程需要将数据放入缓冲区时,首先使用P操作请求空闲位置信号量。如果信号量的值大于0,则可以继续执行;否则,生产者进程会被阻塞,等待缓冲区中有空闲位置。

  3. 生产者进程将数据放入缓冲区后,使用V操作释放已存储数据信号量,将信号量的值加1。

  4. 消费者进程需要从缓冲区中取出数据时,首先使用P操作请求已存储数据信号量。如果信号量的值大于0,则可以继续执行;否则,消费者进程会被阻塞,等待缓冲区中有数据可供取出。

  5. 消费者进程取出数据后,使用V操作释放空闲位置信号量,将信号量的值加1。

下面是一个生动形象的例子:

假设有一个生产者进程和一个消费者进程,共享一个缓冲区,缓冲区大小为5,生产者进程每次向缓冲区中放入1个数据,消费者进程每次从缓冲区中取出1个数据。当缓冲区中没有数据时,消费者进程需要等待数据的生产;当缓冲区中满了时,生产者进程需要等待空闲位置的产生。

定义两个计数型信号量 empty 和 full,分别表示缓冲区中空闲的位置数量和已经存储的数据数量。

empty = 5  # 初始值为缓冲区大小
full = 0   # 初始值为0

 生产者进程:

while True:
    # 生产数据
    data = produce_data()

    # 请求空闲位置信号量
    P(empty)

    # 将数据放入缓冲区
    put_data_into_buffer(data)

    # 已存储数据信号量,将信号量的值加1
    V(full)

 消费者进程:

while True:
    # 请求已存储数据信号量
    P(full)

    # 从缓冲区中取出数据
    data = take_data_from_buffer()

    # 释放空闲位置信号量
    V(empty)

    # 消费数据
    consume_data(data)

 到这里你是不是感觉还差点什么?没错,我们没有对临界区资源设置互斥访问:

更改后代码如下:
 

def produce():
    while True:
        # 生成数据
        data = generate_data()

        # 请求空闲位置信号量
        P(empty)
        # 请求互斥信号量
        p(mutex)

        # 将数据存入缓冲区
        buffer.append(data)

        # 释放互斥信号量
        v(mutex)

        # 释放已存储数据信号量
        V(full)

def consume():
    while True:
        # 请求已存储数据信号量
        p(full)

        # 请求互斥信号量
        p(mutex)

        # 从缓冲区中取出数据
        data = buffer.pop()

        # 释放互斥信号量
        V(mutex)

        # 释放空闲位置信号量
        V(empty)

        # 消费数据
        consume_data(data)

在这个例子中,互斥信号量被用于保护缓冲区的互斥访问,防止生产者和消费者同时访问缓冲区。在生产者进程中,先请求空闲位置信号量,再请求互斥信号量,将数据存入缓冲区后释放互斥信号量和已存储数据信号量;在消费者进程中,先请求已存储数据信号量,再请求互斥信号量,从缓冲区中取出数据后释放互斥信号量和空闲位置信号量。


4.多生产者多消费者问题

问题描述:

桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。用PV操作实现上述过程。

一文搞懂信号量机制(内含消费者生产者问题)_第8张图片

一文搞懂信号量机制(内含消费者生产者问题)_第9张图片

一文搞懂信号量机制(内含消费者生产者问题)_第10张图片


5.吸烟者问题

问题描述:有三个抽烟者和一个供应者。每个抽烟者不停地卷烟抽,组成一根烟需要三种材料:烟草、纸和胶水。三个抽烟者中,第一个有烟草,第二个有纸,第三个拥有胶水。供应者无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,那么供应者可以继续提供另外两种材料,如此重复(让三个抽烟者轮流地抽烟)。

问题分析:

关系分析。供应者与三个抽烟者分别是同步关系。供应着无法同时满足两个或两个以上的抽烟者,所以三个抽烟者是互斥关系。
整理思路。这里有四个进程,一个供应者,三个抽烟者。
信号量设置。信号量offer1,offer2,offer3分别表示烟草和纸的组合、烟草和胶水的组合、纸和胶水的组合。信号量finish用于互斥进行抽烟动作。

代码实现如下:

int i=0;  
semaphore offer1=0;//定义信号量对应烟草和纸的组合  
semaphore offer2=0;//定义信号量对应烟草和胶水的组合  
semaphore offer3=0;//定义信号量对应纸和胶水的组合  
semaphore finish=0;//定义信号量表示抽烟是否完成  
process P1(){  
  if(i==0)  
    V(offer1); //提供烟草和纸  
  else if(i==1)  
    V(offer2);//提供烟草和胶水  
  else if(i==2)  
    V(offer3);//提供纸和胶水  
  任意两种材料放在桌子上;  
  P(finish);  
  i=(i+1)%3;
}  
process P2(){//拥有烟草者  
  while(1){  
   P(offer3);  
   拿纸和胶水,卷成烟,抽掉  
   V(finish);  
  }  
}  
process P3(){//拥有纸者  
  while(1){  
   P(offer2);  
   拿烟草和胶水,卷成烟,抽掉  
   V(finish);  
  }  
}  
process P4(){//拥有胶水者  
  while(1){  
   P(offer1);  
   拿烟草和纸,卷成烟,抽掉  
   V(finish);  
  }  
}  


6.读者写者问题

问题描述:

有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。

一文搞懂信号量机制(内含消费者生产者问题)_第11张图片

1.关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。

2.整理思路。根据各进程的操作流程确定P、V操作的大致顺序

3.设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为1, 同步信号量的初始值要看对应资源的初始值是多少)

两类进程:写进程、读进程
互斥关系:写进程―写进程、写进程―读进程。读进程与读进程不存在互斥问题。

 读者优先算法:

 为实现Reader和Writer进程之间在读或写时的互斥而设置了一个互斥信号量wmutex。再设置一个整型变量conut表示正在读的进程数目。

仅当count=0时,Reader进程才需要执行P(wmutex)操作;同理,仅当Reader进程在执行了count减一操作后其值为0时,才需要执行V(wmutex)操作,以便让Writer进程操作;
由于count是一个可被多个Reader进程访问的临界资源,因此为其设置一个互斥信号量rmutex;
 

semaphore rmutex=1,wmutex=1;
int count=0;
 
void Reader()
{
    while(1)
    {
        P(rmutex);
        if(count==0)
            P(wmutex);
        count++;
        V(rmutex);
 
        read;
        
        P(rmutex);
        count--;
        if(count==0)
           V(wmutex);
        V(rmutex);
    }
}
 
void Writer()
{
    while(1)
    {
        P(wmutex);
        write;
        V(wmutex);
    }
}

 写者优先算法

 所谓写者优先,即:当有读者进程正在执行,写者进程发出申请,这时应该拒绝其他读者进程的请求,等待当前读者进程结束后立即执行写者进程,只有在无写者进程执行的情况下才能够允许读者进程再次运行。为此,增加一个信号量并且在上面的程序中 writer()和reader()函数中各增加一对PV操作,就可以得到写进程优先的解决程序。

在读者优先的基础上

增加信号量r,初值是1,用于禁止所有的读进程。
增加一个记数器,即整型变量writecount,记录写者数,初值是0(原count改为readcount)。 writecount为多个写者共享的变量,是临界资源。用互斥信号量wmutex控制, wmutex初值是1(原wmutex改为mutex1)。
 

int readercount = 0;            //⽤用于记录当前的读者数量量 
int writercount = 0;            //⽤用于控制rsem信号量量 

semaphore rmutex = 1;           //⽤用于保护更更新readercount变量量时的互斥 semaphore wmutex = 1;           //⽤用于保护更更新writercount变量量时的互斥
semaphore rw = 1;               //⽤用于保证读者和写者互斥地访问⽂文件 
semaphore rsem = 1;             //当⾄至少有⼀一个写者申请写数据时互斥新的读者进⼊入读数据

writer() {
   while (1) {
   	 P(wmutex);
   	 if (writercount == 0) {            
   	 	P(rsem);        
   	 }       
   	 writercount++;        
   	 V(wmutex);        
   	 P(rw);        
   	 writing;        
   	 V(rw);        
   	 P(wmutex);        
   	 writercount--;        
   	 if (writercount == 0) {            
   	 	V(rsem);       
   	  }        
   	  V(wmutex);    
    } 
}
   
reader() {   
    while (1) { 
   	 P(rsem);        
   	 P(rmutex);        
   	 if (readercount == 0) {            
   	 	P(rw);       
   	  }        
   	  readercount++;       
   	  V(rmutex);        
   	  V(rsem);        
   	  reading;        
   	  P(rmutex);        
   	  readercount--;       
   	  if (readercount == 0) {           
   	   	 V(rw);        
   	   	}         
   	   	V(rmutex);   
    }
}

读写公平算法

为实现读写公平,我们必须要同时满足以下四个条件:

  1. 读者写者的优先级相同
  2. 读者、写者互斥访问
  3. 只允许有一个写者访问临界区
  4. 可以有多个读者同时访问临界区的资源

        为此,我们设置一个互斥信号量queue,其作用是让Writer进程和Reader进程进行排队,当有Reader进程执行时,queue=0,因此Writer进程会进入等待,直到queue变为1;当Writer进程执行时,同理;因此我们借助queue实现了读写进程的优先级平等。其余的保持3.1中算法不变。
 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
#define N 5
 
int readcount=0,a=5,b=5;
int r[N]={0,1,2,3,4};
sem_t wmutex,rmutex,queue;
 
 
void delay()
{
	int time = rand() % 10 + 1;          //随机使程序睡眠0点几秒           
	usleep(time * 100000);
}
 
void Reader(void *arg)
{
	int i=*(int *)arg;
	while(a>0)
	{
    		a--;
    		delay();
    		
			sem_wait(&queue);		//让写者进程排队,读写进程具有相同的优先级 
        	sem_wait(&rmutex);		//与其他读者进程互斥的访问readcount 
        	if(readcount==0)		//最开始的时候readcount=0 
        		sem_wait(&wmutex);	//与写者进程互斥的访问共享文件 
        	readcount++;
        	sem_post(&rmutex);
			sem_post(&queue);		//使得写者进程进入准备状态 
	
			//Reader
        	printf("Reader%d is reading!\n",i);
        	printf("Reader%d reads end!\n",i);
        	
        	sem_wait(&rmutex);
        	readcount--;
        	if(readcount==0)
        	    sem_post(&wmutex);
        	sem_post(&rmutex);
    	}	
}
 
void Writer()
{
	while(b>0)
    	{
    		b--;
    		delay();
    		
    		sem_wait(&queue); 
        	sem_wait(&wmutex);
    		
    		printf("writer is writing!\n");
        	printf("writer writes end!\n");
    		
        	sem_post(&wmutex);
        	sem_post(&queue);
    	}
}
 
int main()
{
	int i;
	pthread_t writer,reader[N];
	srand((unsigned int)time(NULL));
	
	sem_init(&wmutex,0,1);//互斥锁初始化 
	sem_init(&rmutex,0,1);
	sem_init(&queue,0,1);
	
	for(i=0;i<5;i++)//创建线程 
	{
		pthread_create(&reader[i],NULL,(void *)Reader,&r[i]);
	} 
	
	pthread_create(&writer,NULL,(void *)Writer,NULL);
		
	pthread_join(writer,NULL);//线程等待 
	
	sem_destroy(&rmutex);   //互斥锁的销毁
	sem_destroy(&wmutex);   
	sem_destroy(&queue); 
	
	return 0;
} 

7.哲学家问题

问题描述:

一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。

问题分析:

1.关系分析。系统中有5个哲学家进程,5位哲学家与左右邻居对其中间筷子的访问是互斥关系

2.整理思路。这个问题中只有互斥关系,但与之前遇到的问题不同的事,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的精髓。

3.信号量设置。定义互斥信号量数组chopstick[5]={1,1,1,1,1}用于实现对5个筷子的互斥访问。并对哲学家按0~4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5。

chopstick[5]={1,1,1,1,1}用于实现对5个筷子的互斥访问。并对哲学家按0~4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5。

假设有5个哲学家和5根筷子,可以定义一个二元信号量数组来表示每个筷子是否被使用,以及一个互斥信号量来保证哲学家的就餐过程是互斥的。代码如下:

Semaphore chopsticks[5] = {1, 1, 1, 1, 1}; // 初始值为1,表示筷子可用
Semaphore mutex = 1; // 初始值为1,表示哲学家就餐互斥

void philosopher(int i) {
    while (true) {
        // 思考
        think();
        
        // 尝试获取左边的筷子
        wait(chopsticks[i]);
        
        // 尝试获取右边的筷子
        wait(chopsticks[(i+1)%5]);
        
        // 获取到两个筷子,开始就餐
      
        eat();
    
        
        // 放下筷子,继续思考
        signal(chopsticks[i]);
        signal(chopsticks[(i+1)%5]);
    }
}

这个问题中存在一个潜在的死锁问题,即每个哲学家都拿起了自己左边的筷子,等待右边的筷子被释放,但是右边的哲学家也在等待自己右边的筷子被释放,导致所有哲学家都无法进餐,形成死锁。

我们可以采取下面的方法:

使用AND信号量机制,意思就是如果想给某个哲学家筷子,就将他需要的所有资源都给他,然后让他进餐,否则就一个都不给他。

C语言的库里面并没有给提供AND型信号量,但是我们可以利用互斥量简单的替代一下AND信号量,就是设置一个全局互斥量mutex,用来锁住全部的临界资源,当一个哲学家企图拿筷子的时候,就将所有的资源锁住,然后让他去拿他需要的筷子,等他取到他需要的筷子之后,就解锁,然后让其他哲学家取筷子。代码如下:
 

Semaphore chopsticks[5] = {1, 1, 1, 1, 1}; // 初始值为1,表示筷子可用
Semaphore mutex = 1; // 初始值为1,表示哲学家就餐互斥

void philosopher(int i) {
    while (true) {
        // 思考
        think();
         wait(mutex); // 获取互斥信号量
        // 尝试获取左边的筷子
        wait(chopsticks[i]);
        
        // 尝试获取右边的筷子
        wait(chopsticks[(i+1)%5]);
         signal(mutex); // 释放互斥信号量
        // 获取到两个筷子,开始就餐
       
        eat();
       
        
        // 放下筷子,继续思考
        signal(chopsticks[i]);
        signal(chopsticks[(i+1)%5]);
    }
}

8.总结

信号量(Semaphore)是一种用于实现多线程之间同步的机制。它是由荷兰计算机科学家Dijkstra于1965年首先提出的。Semaphore可以控制同时访问某个资源的线程数量,以保证多个线程能够正确、合理的使用共享资源。

Semaphore有两种类型:二元信号量和计数信号量。二元信号量也称为互斥量(Mutex),用于保护共享资源的互斥访问,只允许一个线程访问该资源。计数信号量用于控制某个共享资源的可用数量,允许多个线程同时访问该资源,但需要控制访问数量,以避免竞争和冲突。

Semaphore通常提供两个操作:P操作(申请资源)和V操作(释放资源)。当一个线程需要使用某个共享资源时,需要执行P操作申请该资源,如果资源不可用,则线程进入阻塞状态,直到该资源可用;当线程使用完共享资源后,需要执行V操作释放该资源,使其它线程可以继续使用。

Semaphore机制可以很好地解决线程间资源竞争的问题,但如果使用不当,也容易导致死锁等问题。因此,在编写多线程程序时,需要仔细设计和使用Semaphore,以保证程序的正确性。

你可能感兴趣的:(操作系统学习笔记,java,开发语言)