经典进程同步和互斥问题

基本概念
  • 进程同步: 对多个进程在执行次序上进行协调,使并发执行的各进程间能按照一定的规则共享系统资源,以免进程无序争夺资源而导致系统混乱。
  • 进程互斥: 某一时刻不允许多个进程同时访问临界资源,只能单个进程访问
一、生产者消费者问题

在这里插入图片描述
在此问题中,需要注意以下几点:

  1. 只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待
  2. 只有缓冲区不空时,消费者才能从缓冲区取出产品,否则必须等待
  3. 缓冲区属于临界资源,各进程必须互斥地进行访问

那么如何用P、V操作实现这些要求呢?
我们以2个生产者、3个消费者,缓冲区大小为4举例

针对要求1、2:
  • 设置同步信号量empty、full,初始值分别为4、0,表示刚开始时缓冲区中还没有数据
  • 生产者每次要消耗(P)一个空闲(empty)缓冲区,并生产(V)一个产品(full)
  • 消费者每次要消耗(P)一个产品(full),并释放(V)一个空闲(empty)缓冲区
针对要求3:
  • 设置互斥信号量mutex,初始值为1,表示同一时刻只有一个进程能访问缓冲区

前驱图表示:
在这里插入图片描述

伪代码如下:

def producer():
	while True:
		生产一个产品
		P(empty)  # P操作,表示需要消耗一个非空缓冲区
		P(mutex)   # 为了互斥访问缓冲区
		把产品放入缓冲区  # 用互斥锁实现此缓冲区的互斥访问
		V(mutex)
		V(full)  # 对信号量full加1,表示生产了一个数据

def consumer():
	while True:
		P(full)  # P操作,表示消耗一个数据
		P(mutex)  
		从缓冲区取走产品
		V(mutex)
		V(empty)  # V操作,表示释放一个空闲缓冲区
		使用产品

给出Python的实现代码

import threading
from multiprocessing import Semaphore
import multiprocessing

mutex = Semaphore(1)  # 用于实现各个进程互斥访问缓冲区,任何时刻缓冲区
empty = Semaphore(4)  # 4个缓冲区
full = Semaphore(0)  # 表示当前缓冲区的资源数量
in_index = 0
out_index = 0
buffer = [0, 0, 0, 0]
n = len(buffer)


def produce(id):
    global in_index
    data = 1
    while True:
        empty.acquire()  # P操作,表示需要消耗一个非空缓冲区
        mutex.acquire()  # 为了互斥访问缓冲区
        buffer[in_index] = data
        print("%d号生产者进程放入数据%d,放在了%d位置" % (id, data, in_index))
        in_index = (in_index + 1) % n
        data += 1
        mutex.release()  # 表示已经访问完缓冲区
        full.release()  # 对信号量full加1,表示使用了一个空闲缓冲区,生产了一个数据


def consume(id):
    global out_index
    while True:
        full.acquire()  # P操作,表示消耗一个数据
        mutex.acquire()  # 实现对缓冲区的互斥访问
        data = buffer[out_index]
        buffer[out_index % n] = 0  # 取出数据
        print("%d号消费者从%d位置取得数据%d" % (id, out_index, data))
        out_index = (out_index + 1) % n
        mutex.release()
        empty.release()  # V操作,表示释放一个空闲缓冲区


def main():
    consumer1 = threading.Thread(target=consume, args=(1,))
    consumer2 = threading.Thread(target=consume, args=(2,))
    consumer3 = threading.Thread(target=consume, args=(3,))
    consumer1.start()
    consumer2.start()
    consumer3.start()
    producer1 = threading.Thread(target=produce, args=(1,))
    producer2 = threading.Thread(target=produce, args=(2,))
    producer1.start()
    producer2.start()


if __name__ == '__main__':
    main()

注:写代码时需要注意,往缓冲区放入或是从缓冲区取走数据的索引都应该是全局的,若是每个进程(代码实现其实是线程)都有自己访问缓冲区的索引,一定会导致数据覆盖以及重复读取某个数据的情况

思考:能否改变实现同步和实现互斥的P操作

就像这样,把实现互斥的P操作放在实现同步的P操作之前

def producer():
	while True:
		生产一个产品
		P(mutex)  # 1
		P(empty)  # 2 
		把产品放入缓冲区  # 用互斥锁实现此缓冲区的互斥访问
		V(mutex)
		V(full)

def consumer():
	while True:
		P(mutex)  # 3
		P(full)  # 4
		从缓冲区取走产品
		V(mutex)
		V(empty)
		使用产品

如果执行顺序就像伪代码中的注释那样,且在某一时刻缓冲区满,即 empty = 0
此时生产者进程会顺利对互斥信号量进行减1操作,然后阻塞在标号为 2 的地方
此时生产者进程再执行,将会阻塞在标号为 3 的地方,即互斥信号量的地方
这就形成了 “死锁”

总结一下:实现互斥的P操作必须放在实现同步的P操作之后

小Tips:实际操作中在临界区的代码也应尽可能的少,否则会增大对临界区的上锁时间,即其他进程的等待时间,从而导致系统并发度降低

二、读者——写者问题

在这里插入图片描述

关于此问题需要注意的地方:
  1. 允许多个读进程同时访问共享文件。因为读操作不会改变共享文件的改变
  2. 同一时刻只允许一个写进程访问共享文件。若多个写进程同时访问,则可能导致数据覆盖
  3. 写进程进行写操作时,不允许读进程进行读操作。因为写进程改变共享文件,此时读进程读取的数据可能并不是自己之前想要读取的数据

伪代码如下:

read_mutex = Semaphore(1)
write_mutex = Semaphore(1)
read_count = 0  # 表示正在进行读操作的读进程数量

def reader():
	while True:
		P(read_mutex)  # 上读锁,防止多个读进程同时访问下面的两段代码
		if read_count == 0:
			P(write_mutex)  # 第一个读进程上写锁,防止写进程与读进程同时访问文件
		read_count += 1
		V(read_mutex)
		
		读进程并发读文件
		
		P(read_mutex)
		read_count -= 1
		if read_count == 0:
			V(write_mutex)  # 最后一个读进程释放写锁,表示此时没有读操作,可以让写进程开始操作文件
		V(read_mutex)

def writer():
	while True:
		P(write_mutex)
		写进程写文件
		V(write_mutex)

在上述伪代码中,需要注意的是下面的代码必须一气呵成
在这里插入图片描述
假如不用一对PV操作将这段代码设置为临界区,将可能会有两个读进程都进入if语句,其中一个对write_mutex进行P操作后,另一个读进程将会阻塞在对write_mutex的P操作,这将违背 条件1:允许多个读进程同时访问文件

而且,read_count的读取,赋值,写回操作也必须一气呵成,因为该值是读进程的计数,直接影响write_mutex的PV操作,也就影响到条件2、3

实现代码如下:

import threading
from multiprocessing import Semaphore
import multiprocessing
import time


read_mutex = Semaphore(1)
write_mutex = Semaphore(1)
read_count = 0


class Reader(threading.Thread):
    def __init__(self, id):
        super().__init__()
        self.id = id

    def run(self):
        while True:
            time.sleep(1)
            global read_count
            print("%d号读线程正在等待..." % self.id)
            read_mutex.acquire()  # 读进程上读锁
            if read_count == 0:
                write_mutex.acquire()  # 第一个读进程上写锁,不允许写进程写文件
            read_count += 1  # 访问的读进程 +1
            read_mutex.release()  # 释放读锁

            # 由于这里的读锁完全释放,可以允许多个读进程访问
            print("%d号读线程正在读文件..." % self.id)

            # 为了使得read_count不被多个读进程同时访问,上读锁
            read_mutex.acquire()
            read_count -= 1  # 访问的读进程 -1
            if read_count == 0:
                write_mutex.release()  # 当没有读进程读文件时,释放写锁,使得写进程可以上锁
            read_mutex.release()  # 释放读锁
            print("%d号读线程完成读取操作..." % self.id)


class Writer(threading.Thread):
    def run(self):
        while True:
            time.sleep(1)
            global read_count
            print("---写线程正在等待...")
            write_mutex.acquire()
            print("---写线程正在写文件...")
            write_mutex.release()
            print("---写线程完成操作...")


def main():
    for i in range(5):
        read_thread = Reader(i)
        read_thread.start()
    write_thread = Writer()
    write_thread.start()


if __name__ == '__main__':
    main()

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在多线程编程中,多个线程可以同时运行,但是它们可能需要访问同一资源,这时就需要进行同步和互斥,以保证程序的正确性。常用的方法包括使用信号量、互斥锁和条件变量。 1. 信号量 信号量是一种在多线程编程中实现同步的方法。信号量记录了某个共享资源的状态,用于控制线程的执行顺序和访问资源的方式。信号量有两种类型:二进制信号量和计数信号量。 二进制信号量只有两种状态:0和1。当信号量为0时,线程会被阻塞。当信号量为1时,线程可以继续执行。二进制信号量通常用于实现互斥。 计数信号量可以有多种状态。当信号量为0时,线程会被阻塞。当信号量为正数时,线程可以继续执行。当信号量为负数时,线程会被阻塞,并且可以唤醒其他线程。计数信号量通常用于实现同步。 2. 互斥互斥锁是一种在多线程编程中实现互斥的方法。互斥锁是一个二进制标志,用于控制某个共享资源只有一个线程可以访问。当一个线程获得了互斥锁,其他线程需要等待,直到该线程释放互斥锁。 在使用互斥锁时,需要注意避免死锁的问题。死锁发生在多个线程互相等待对方释放资源的情况下。为了避免死锁,可以遵循以下原则: - 一个线程只能持有一个锁,否则容易出现死锁。 - 尽量避免在锁内部执行耗时操作。 - 获得锁的顺序应该是固定的,例如按照某个资源的编号来获得锁。 3. 条件变量 条件变量是一种在多线程编程中实现同步的方法。条件变量用于实现线程的等待和唤醒,以实现同步和互斥。条件变量通常与互斥锁一起使用,以实现线程的同步。 在使用条件变量时,需要注意避免竞态条件的问题。竞态条件发生在多个线程同时访问共享资源,并且对资源的访问顺序不确定的情况下。为了避免竞态条件,可以使用互斥锁来保护共享资源,并使用条件变量来实现线程的等待和唤醒。 总之,使用信号量、互斥锁和条件变量等方法可以有效地解决多线程编程中的同步和互斥问题。但是,在使用这些方法时,需要注意避免死锁和竞态条件等问题

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bugcoder-9905

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值