python多进程并发编程之互斥锁与进程间的通信
一、互斥锁
1 2 3 | 多个进程之间的内存空间是隔离的,但是硬盘,数据库,打印终端都是共享的 。因此当多个进程同时修改硬盘中的同一个文件,或者修改数据库中的同一条记录时,就存在资源竞争的问题,容易出错。 加锁的目的就是为了保证多个进程修改同一块数据时,同一时间只能有一个修改,即串行的修改,没错,速度是慢了,牺牲了速度而保证了数据安全。 |
进程互斥锁代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import os import json import time import random from multiprocessing import Process,Lock def buy_ticket(mutex): time.sleep(random.randint(1,3)) #模拟延迟 ticket_info = json.load(open( 'ticket_info' , 'r' ,encoding= 'utf-8' )) mutex.acquire() if ticket_info[ "ticket_count" ] > 0: ticket_info[ "ticket_count" ] -= 1 json.dump(ticket_info,open( 'ticket_info' , 'w' ,encoding= 'utf-8' )) print( "PID %s 买到票了" % os.getpid()) else : print( "pid %s 没买到票,票卖完了!" % os.getpid()) mutex.release() if __name__ == '__main__' : mutex = Lock() for i in range(10): p = Process(target=buy_ticket,args=(mutex,)) p.start() |
执行结果:
1 2 3 4 5 6 7 8 9 10 | PID 10632 买到票了 pid 3808 没买到票,票卖完了! pid 15488 没买到票,票卖完了! pid 556 没买到票,票卖完了! pid 10448 没买到票,票卖完了! pid 11672 没买到票,票卖完了! pid 8400 没买到票,票卖完了! pid 5340 没买到票,票卖完了! pid 11164 没买到票,票卖完了! pid 9140 没买到票,票卖完了! |
通过上面这个例子,我们知道互斥锁可以一个任务中的某个子任务由并行改为串行,而不是将整个任务改成串行。
总结:
多个进程的内存是相互隔离的,但硬盘,数据库等确实共享的。通过互斥锁就可以实现多个进程之间的通信,并且不会造成数据混乱,保证的数据的安全。但互斥锁将并发改为了串行,降低了效率 ,而且需要我们自己加锁,释放锁,容易出现问题。
二、IPC机制
进程间通信(IPC,InterProcess Communication)
1 2 3 | 是指在不同进程之间传播或交换信息。是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。 IPC的方式通常有管道(PIPE)(包括无名管道和命名管道)、消息队列、信号量、共享内存、套接字(Socket)、Streams、旗语等。其中 Socket和Streams支持不同主机上的两个进程IPC。 |
进程间通信(IPC)常见分类的特点
1.管道
1 | 管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。 |
特点:
1 2 3 | 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。 |
2.FIFO (命名管道)
1 | FIFO,也称为命名管道,它是一种文件类型。 |
特点:
1 2 | FIFO可以在无关的进程之间交换数据,与无名管道不同。 FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。 |
3.消息队列
1 | 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。 |
特点:
1 2 3 | 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。 |
4.信号量
1 | 信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。 |
特点:
1 2 3 4 | 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。 支持信号量组。 |
5.共享内存
1 | 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。 |
特点:
1 2 3 | 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。 因为多个进程可以同时操作,所以需要进行同步。 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。 |
参考文档:https://blog.csdn.net/python_jw/article/details/79506702
三、使用队列实现进程间通信
1.Queue通信机制
首先讲解 一下Queue通信方式。Queue是多进程安全的队列,可以使用Queue实现多进 程之间的数据传递,
通信原理:在内存中建立队列数据结构模型。多个进程都可以通过队列存入内容,取出内容的顺序和存入的顺序保持一致
有两个方法: Put和Get可以进行Queue操作:
put:
1 | Put方法用以插入数据到队列中,它还有两个可选参数:blocked和timeout。如果blocked为True (默认值),并且timeout为正值,该方法会阻塞timeout指定的时间, 直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该 Queue已满,会立即抛出Queue.Full异常。(即如果blocked为True时,如果队列满,会有一定的等待时间) |
get:
1 | Get方法可以从 队列读取并且删除一个元素 。同样,Get方法有两个可选参数:blocked 和timeout。如果blocked为True (默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False, 分两种情况:如果Queue 有 一个值可用, 则立即返回该值;否则,如果队列为空, 则立即抛出 Queue.Empty异常。(形式上与Put相同,不同的仅仅是写入和获取的区别) |
2.代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | from multiprocessing import Process,Lock,Queue def put(queue): for i in range(5): queue.put(i) def get (queue): for i in range(5): print( "获取了 %s" % queue. get ()) if __name__ == '__main__' : queue = Queue(5) p1 = Process(target=put,args=(queue,)) p2 = Process(target= get ,args=(queue,)) p1.start() p2.start() |
四、生产者消费者模型介绍
什么是生产者和消费者模式
1 2 3 4 5 | 生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列, 消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。 这个阻塞队列就是用来给生产者和消费者解耦的 |
为什么要使用生产者消费者模型
1 2 3 | 生产者指的是生产数据的任务,消费者指的是处理数据的任务,在并发编程中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。 同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。 |
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | from multiprocessing import Process, Queue import time def producer(q, name, food): count = 0 while True: count +=1 res = '%s,%s' % (food, count) time.sleep(1) # 生产food得有个过程,就先让睡一会 print( '生产者[%s] 生产了 [%s]' % (name, res)) q.put(res) def consumer(q, name): while True: res = q. get () if res is None: break time.sleep(1) print( '消费者[%s]吃了[%s]' % (name, res)) if __name__ == '__main__' : # 容器 q = Queue() p = Process(target=producer, args=(q, 'egon' , '包子' )) c = Process(target=consumer, args=(q, 'alex' ,)) p.start() c.start() |
运行结果:
1 2 3 4 5 6 | 生产者[egon] 生产了 [包子,0] 生产者[egon] 生产了 [包子,1] 消费者[alex]吃了[包子,0] 生产者[egon] 生产了 [包子,2] 消费者[alex]吃了[包子,1] 消费者[alex]吃了[包子,2] |
"一劳永逸" 的话,有是有的,而 "一劳永逸" 的事却极少
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」