目录
**1.**设计目标 *3*
2.3等待操作、避免反复访问、模仿实际过程的时间消耗、控制线程的执行速度 4
1.设计目标
1.1互斥访问缓冲区
确保在任何时刻,只有一个生产者或消费者能够同时访问缓冲区(桌上)的数据(产品),以防止数据竞争和不一致。否则会导致数据竞争,即当多个线程或进程同时访问和修改共享资源(如缓冲区中的数据)时,可能会导致数据不一致或错误的结果。
为了实现互斥访问,我选择使用互斥锁(mutex)。当一个线程持有锁时,其他线程必须等待,直到锁被释放。这样可以确保在任何给定时间点上,只有一个线程能够访问缓冲区。
因此,当生产者向缓冲区添加数据,而消费者从缓冲区取出数据。由于互斥锁的存在,这些操作不会同时发生,从而保证了数据的一致性和完整性。
1.2同步操作
生产者只有在缓冲区未满时才能生产(放入)数据;而消费者只有在缓冲区不为空时才能消费(取出)数据。这样才能保证缓冲区不会崩溃,不会出现生产者没处放商品,而消费者没法拿商品的场景。
为了实现同步操作,我使用的是条件判断,对于生产者,判断缓冲区中empty(空位)是否为0,如果为0,则等待,否则生产;对于消费者,判断缓冲区中full(已有商品)是否为0,如果为0,则等待,否则消费。总而言之,通过判断缓冲区的状态(如空、满),生产者和消费者可以根据当前状态来决定是否进行生产或消费操作。
1.3等待操作
在实现同步操作的基础上,通过控制一方等待,来使其接下来的操作能满足要求。例如,当消费者进行消费,但是缓冲区没有数据,消费者需要等待随机时间,直到生产者生产数据、放入缓冲区,才能进行消费。
1.4避免反复访问
通过等待操作,能减少系统的繁忙和不必要操作。例如在等待操作中提到,缓冲区没有数据,让消费者等待随机时间,而不是让消费者又重新访问锁、重新判断缓冲区数据数量,这样能提高系统的并发性和吞吐量。
1.5模仿实际生产/消费过程的时间消耗
在真实场景中,生产者生产商品和消费者消费商品都需要一定的时间。使用time.sleep() 模拟这些过程的时间消耗,使得模拟更加贴近实际情况。
1.6控制线程的执行速度
由于生产者和消费者线程是并发执行的,如果没有适当的延迟,它们可能会以非常高的速度交替执行,导致对共享资源(缓冲区、锁)的频繁竞争。这种频繁的竞争不仅会降低程序的性能,还可能引发更多的线程同步问题。通过引入 time.sleep(),可以适当降低线程的执行速度,从而减少资源竞争,提高程序的稳定性和性能。
2.功能描述
2.1互斥访问缓冲区
选择使用互斥锁(mutex),定义down()、up()函数。前者作用是判断锁是否小于等于0,若满足条件,则输出“需要等待”;否则,将信号量(锁)进行减一操作,表示上锁;后者作用是将信号量(锁)进行加一操作,表示上锁。
例如,当生产者先访问缓冲区时,会先将mutex减一(mutex初始值为1),进行上锁,这样如果消费者要访问缓冲区,锁等于0,而被拒绝访问,输出“需要等待”。当生产者访问缓冲区完成后,会执行down()函数,将锁加一,这样消费者才能成功访问,否则生产者会一直占用资源而消费者会一直等待。
这样设计后,当生产者向缓冲区添加数据,而消费者从缓冲区取出数据。由于互斥锁的存在,这些操作不会同时发生,从而保证了数据的一致性和完整性。
2.2同步操作
使用条件判断,对于生产者,判断缓冲区中empty(空位)是否为0,如果为0,则等待,否则生产;对于消费者,判断缓冲区中full(已有商品)是否为0,如果为0,则等待,否则消费。设置条件判断的目的是为了保证缓冲区数据不会溢出,防止系统崩溃。
2.3等待操作、避免反复访问、模仿实际过程的时间消耗、控制线程的执行速度
使用time.sleep()函数控制生产者/消费者进程进行等待。
例如当消费者进行消费,但是缓冲区没有数据,消费者需要等待随机时间,即执行time.sleep(random.uniform(0.1,1)),直到生产者生产数据、放入缓冲区,才能进行消费。这样通过控制一方等待另一方执行,来从而使其接下来的操作能满足要求,并避免该方一直访问而访问失败,从而避免反复访问。
例如在真实场景中,生产者生产商品和消费者消费商品都需要一定的时间。使用time.sleep() 模拟这些过程的时间消耗,使得模拟更加贴近实际情况。
例如通过引入 time.sleep(),可以适当降低线程的执行速度,从而减少资源竞争,提高程序的稳定性和性能。
3.总体设计
3.1全局变量定义
定义初始值:
锁mutex=1;空位置empty=100;已生产产品数=0;生产列表products=[];消费列表consumer_have=[]
3.2 down()函数
处理三种不同类型的资源锁(mutex、empty、full)。对于每种锁,使用with lock语句确保线程安全地修改全局变量。对于mutex,它检查其是否小于等于0,如果是,则输出“需要等待”,否则,减少锁计数;对于empty和full,分别减少它们的计数。
3.3 up()函数
这段代码定义了一个名为up的函数,用于处理三种资源状态(mutex、empty、full)的“开锁”或增加操作。对于每种状态,都使用线程锁lock来确保安全地增加相应的全局变量计数。
3.4 producer()生产者函数
模拟一个无限循环的生产过程。
生产者首先请求生产,并随机生成一个1到100之间的整数值作为要生产的物品。
在生产之前,生产者对缓冲区进行上锁操作,以确保同时只有一个生产者能够进行生产。如果此时缓冲区已满(empty==0),则生产者无法生产,会先解锁然后等待一段时间(模拟等待消费者消费);如果缓冲区有空位,生产者会继续执行生产操作,减少一个空位,并将生成的物品添加到名为products的列表中。
生产完成后,生产者解锁生产区域和增加桌上已满物品的数量。最后,生产者打印当前锁的状态、空位数、桌上物品数量以及已生产的物品列表,并等待一段时间后再进行下一轮生产。
3.5 consumer()消费者函数
模拟一个无限循环的消费过程。
消费者首先请求消费,如果桌上没有商品,则消费者会等待一段时间。
当桌上有商品可供消费时,消费者对缓冲区进行上锁操作,以确保只有消费者对其进行操作。随后,消费者从products列表中取出一个商品进行消费,并减少缓冲区物品的数量。
消费完成后,消费者解锁缓冲区并增加其空位数量。在消费过程中,消费者还将消费的商品添加到consumer_have的列表中,以记录其拥有的商品。
最后,消费者打印当前锁的状态、桌上剩余商品数量、桌上空位数量、已消费的商品以及消费者拥有的商品列表,并等待一段时间后再进行下一轮消费。
4.详细设计
4.1 完整流程图
生产者-消费者模型是一种经典的并发编程模式,其中生产者线程负责生成数据并将其放入共享缓冲区,而消费者线程则从共享缓冲区中取出数据进行处理。整个流程(如图1)通过锁和计数器等同步机制来确保多个线程能够安全地访问共享资源,避免数据竞争和不一致的问题。
生产者线程首先会请求进入生产状态,并尝试获取锁以访问共享缓冲区。如果锁已被其他线程占用,生产者将等待直到锁可用。一旦获取到锁,生产者会检查是否有足够的空位来生产新商品。如果没有空位,生产者将释放锁并等待一段时间;如果有空位,生产者将生产一个随机值的商品,并将其添加到共享缓冲区中。然后,生产者会更新相应的计数器,以反映缓冲区中商品数量的变化。完成这些操作后,生产者会释放锁,并等待一段时间以模拟生产过程中的延迟。这个过程会不断重复,直到生产者被优雅地停止。
消费者线程首先会请求进入消费状态,并尝试获取锁以访问共享缓冲区。如果锁已被其他线程占用,消费者将等待直到锁可用。一旦获取到锁,消费者会检查缓冲区中是否有可供消费的商品。如果没有商品,消费者将释放锁并等待一段时间;如果有商品,消费者将从缓冲区中取出一个商品进行处理,并更新相应的计数器以反映缓冲区中商品数量的变化。完成这些操作后,消费者会释放锁,并将消费的商品添加到自己的列表中。然后,消费者会等待一段时间以模拟消费过程中的延迟。这个过程也会不断重复,直到消费者被停止。
图1 生产者-消费者完整流程图
4.2 库函数调用和全局变量定义
引入模块例如time用于等待、threading用于线程执行,并定义了桌上空位、锁状态、桌上商品数量以及生产和消费的商品列表。如图2。
图2库函数调用和全局变量定义代码
4.3 down()函数
通过对信号量(锁)的修改(即mutex-1,表示开锁),以及判断锁的状态,实现对共享资源数据缓冲区的访问,避免生产者和消费者两方同时修改数据缓冲区。并且简化程序,便于处理empty、full的数据。如图3。
输入参数:函数接收一个字符串参数S,该参数用于指示要操作的数据。
锁定检查:如果S等于’mutex’,则执行以下步骤:
a. 使用with lock:语句自动获取锁。这确保了在当前线程执行完步骤c之前,其他线程无法获取锁。
b. 检查全局变量mutex的值。如果mutex小于或等于0,表示锁已被其他线程占用,因此打印“等待…”消息,并通过time.sleep(0.1)添加一个短暂的延迟,以避免生产者忙等待占用过多CPU资源
c. 如果mutex大于0,表示锁是空闲的,因此将mutex的值减1以锁定数据缓冲区的资源。
空位或减少商品计数:
如果S等于’empty’,则在锁的保护下将全局变量empty的值减1,表示桌上空位数量减少了一个。
如果S等于’full’,则在锁的保护下将全局变量full的值减1,表示桌上商品数量减少了一个。
图3 down()函数代码
4.4 up()函数
定义up函数,用于解锁操作。它接收一个字符串S作为参数,该字符串可以是’mutex’、‘empty’或’full’。根据S的值,函数会全局增加相应的计数器(mutex、empty、full),用于多线程或并发环境下的资源管理和同步。
4.5 producer()生产者函数
模拟生产者线程的行为:生产者线程负责生成产品并将其放入缓冲区(列表products)。
无限循环生产:生产者线程在一个无限循环中运行,这意味着它将不断地尝试生产产品。使用random.randint(1, 100)生成一个1到100之间的随机整数作为产品。
请求访问共享资源:调用down(‘mutex’)函数来尝试对缓冲区上锁,模拟了生产者进入临界区的过程,确保在某一时刻只有一个生产者或消费者可以访问共享资源。
在获取互斥锁后,生产者检查empty的值。如果empty为0,表示缓冲区已满,没有空位来放置新产品,生产者就释放互斥锁(up(‘mutex’)),然后等待一段时间(time.sleep(0.3)),以模拟生产者等待消费者消费产品的过程。
如果缓冲区有空位(empty不为0),生产者将继续生产。调用down(‘empty’)函数来减少空位的数量(模拟将一个空位用于放置新产品),并将生成的产品添加到缓冲区列表products中。调用up(‘full’)函数来增加缓冲区中已生产产品的数量。
在完成产品生产和状态更新后,生产者释放互斥锁(up(‘mutex’)),以允许其他生产者或消费者访问共享资源。在生产完一个产品后,等待一段时间(time.sleep(random.uniform(0.1, 1))),以模拟生产过程中的随机延迟。
![]() |
图5 producer()生产者函数代码
4.6 consumer()消费者函数
模拟消费者线程的行为:消费者线程负从缓冲区拿取产品放入自己的消费者列表(列表consumer_have)。
消费者首先检查桌上(缓冲区)是否有商品(full变量表示商品数量)。如果桌上(缓冲区)没有商品,消费者将等待一段时间(0.3秒),然后再次检查。
一旦发现桌上(缓冲区)有商品,消费者通过获取互斥锁(mutex)来确保在拿走商品时不会与其他消费者发生冲突。获取锁后,消费者打印出已上锁的信息。
消费者从桌上(products列表)拿走一个商品,并模拟消费过程。同时,更新桌上商品数量(full减1)和桌上空位数量(empty加1),并释放互斥锁。
消费者将拿走的商品添加到自己拥有的列表中(consumer_have),并打印相关信息,包括桌上剩余商品数量、桌上空位数量、消费者拥有的商品等。
为了模拟真实场景中的消费时间差异,消费者在完成一次消费后会随机等待一段时间(0.1到1秒之间),然后重复上述过程
![]() |
图6 consumer()消费者函数代码
4.7 主函数函数
通过if name == “main”:判断当前脚本是否作为主程序运行。
接着,利用threading.Thread类分别创建生产者线程和消费者线程,其中target参数指定了线程要执行的函数,即producer和consumer。
通过调用start()方法,启动生产者消费者线程,使它们并行执行。
最后,使用join()方法等待这两个线程执行完毕,确保主程序在所有线程结束后再继续或退出。这有助于保持数据的一致性和完整性。
![]() |
图7 主函数代码
5.实现
5.1编译截图
在Windows PowerShell环境中执行,启动一个Python脚本的调试会话。使用debugpy库(一个Python调试适配器)来监听端口54808,并运行位于D:\07042202 2022214517\os\os.py的Python脚本。
图8 编译截图
5.2运行截图
点击运行,程序开始模仿生产者、消费者不停访问缓冲区。
首先,生产者请求生产、消费者请求消费,但由于数据缓冲区没有数据,消费者请求失败,生产者开始随机生产一个序号为78的产品,并放入了数据缓冲区,并正确输出各个相关数值,最后对数据缓冲区进行解锁。
接着,消费者进程请求消费,进行上锁,拿走生产者已生产的78号产品,完成操作后进行解锁。
生产者消费者再反复执行具体过程,查看执行结果、输出结果,双方进行实现同步、互斥访问共享资源,一方上锁,另一方直至其解锁才能进行访问,并且数据缓冲区的数据没有出现错误,如此保证了数据的一致性和完整性。
程序编写、执行正确。
![]() |
![]() |
![]() |
![]() |
图9 运行截图
源代码:
import random
import threading
import time # 引入time模块以便在测试和调试时添加延迟
empty = 100 # 定义桌上空位为100
mutex = 1 # 定义锁的初始值为1
full = 0 # 定义桌上有多少个商品
products = [] # 定义生产列表
consumer_have = [] # 定义消费者拥有的商品列表
def down(S): # 上锁
global mutex, empty, full
if S == 'mutex':
if mutex <= 0: # 如果锁还未开,就等待(忙等待,实际应使用条件变量或其他同步机制)
print('等待........')
time.sleep(0.1) # 添加小延迟以避免忙等待占用过多CPU资源
else: #锁开则上锁
mutex -= 1
elif S == 'empty':
empty -= 1
elif S == 'full':
full -= 1
def up(S): # 开锁
global mutex, empty, full
if S == 'mutex':
mutex += 1
elif S == 'empty':
empty += 1
elif S == 'full':
full += 1
def producer():
while True: # 无限循环生产
print(".....生产者请求生产.....\n")
item=random.randint(1, 100) #定义随机生产的值,假设其为产品
down('mutex') #生产者正在生产,对其进行上锁
print("锁为",mutex,"生产者已上锁")
if(empty==0): #没有空位,无法存放产品
print("没有空位,生产者无法生产")
up('mutex') #开锁
time.sleep(0.3) #等待消费者拿走物品
else:
print(".....生产者正在生产.....")
down('empty') #生产一个物品,空位empty-1
print("空位为",empty,"个")
products.append(item) #向已生产列表中插入刚才生产的值
up('full') #full桌上数量加1
print("桌上还有",full,"个。","桌上商品为",products,"\n")
up('mutex') #开锁
print("锁为",mutex,"生产者已解锁")
time.sleep(random.uniform(0.1, 1))
def consumer():
while True:
print(".....消费者请求消费.....")
if(full==0):
time.sleep(0.3) #如果桌上没有商品,消费者需要等待
else:
down('mutex') #消费者正在拿走产品,对其进行上锁
print("锁为",mutex,"消费者已上锁")
print(".....消费者正在消费.....")
out=products.pop(0) #消费者拿走一个
down('full') #消费者拿走后,桌上数量-1
print("桌上还有",full,"个")
up('mutex') #消费者解锁
print("锁为",mutex,"消费者已解锁")
up('empty') #消费者拿走一个,空位加1
print("桌上空位",empty)
consumer_have.append(out) #放入消费者列表
print("消费者拿走",out,"桌上商品为",products,"消费者拥有",consumer_have,"\n")
time.sleep(random.uniform(0.1, 1))# 添加延迟以避免过快消费
if __name__ == "__main__":
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)# 创建生产者和消费者线程
producer_thread.start()
consumer_thread.start() # 启动线程
producer_thread.join()
consumer_thread.join() # 等待线程
ucts,"消费者拥有",consumer_have,"\n")
time.sleep(random.uniform(0.1, 1))# 添加延迟以避免过快消费
if __name__ == "__main__":
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)# 创建生产者和消费者线程
producer_thread.start()
consumer_thread.start() # 启动线程
producer_thread.join()
consumer_thread.join() # 等待线程