1. Lock(互斥锁)

是可用的最低级的同步指令。Lock处于锁定状态时,不被其他的线程拥有。

from multiprocessing import Process, Value, Lock

def func1(num, lock: Lock):
lock.acquire()
print(num.value)
num.value += 1
lock.release() if __name__ == '__main__':
lock = Lock() # 创建锁对象
val = Value('i', 0)
proc = [Process(target=func1, args=(val, lock)) for i in range(10)]
for p in proc:
p.start()
for p in proc:
p.join() print(val.value) # 10

上面获取锁和释放锁可以用with语句简化,它可以自动获取和释放锁

def func1(num, lock):
with lock:
num.value += 1

输出:

0
1
2
3
4
5
6
7
8
9
10

2. RLock(递归锁)

是一个可以被同一个进程请求多次的同步指令。RLock使用了“拥有的进程”和“递归等级”的概念,处于锁定状态时,RLock被某个进程拥有。拥有RLock的进程可以多次调用acquire(),释放锁时需要调用release()相同次数,其他进程在此期间得不到锁。

主要就是对于进程处理中会有一些比较复杂的代码逻辑过程,比如很多层的函数调用,而这些函数其实都需要进行加锁保护数据访问。

这样就可能会反复的多次加锁,因而用RLock就可以进行多次加锁、解锁,直到最终锁被释放;而如果用普通的Lock,当你一个函数A已经加锁,它内部调用另一个函数B,如果B内部也会对同一个锁加锁,那么这种情况就也会导致死锁。而RLock可以解决这个问题。

from multiprocessing import Process
from multiprocessing import RLock, Lock, Value def f(v, lock):
with lock:
g(v, lock) def g(v, lock):
with lock:
for _ in range(10):
v.value += 1
print(f"g()===>{v.value}") if __name__ == '__main__':
# lock = Lock() # 如果用Lock,就会出现死锁
lock = RLock()
v = Value("i", 0)
p = Process(target=f, args=(v, lock))
p.start()
p.join()
print(v.value) # 10

输出:

g()===>1
g()===>2
g()===>3
g()===>4
g()===>5
g()===>6
g()===>7
g()===>8
g()===>9
g()===>10
10

3. Semaphore(信号量同步)

互斥锁同时只允许一个进程更改数据,而信号量Semaphore是同时允许一定数量的进程更改数据 。

信号量同步基于内部计数器,每调用一次acquire(),计数器减1;每调用一次release(),计数器加1,当计数器为0时,acquire()调用被阻塞。

这是Dijkstra信号量概念PV操作的Python实现。信号量同步机制适用于访问像服务器这样的有限资源,它与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念。

【示例·KTV】

假设商场里有4个迷你唱吧,所以同时可以进去4个人,如果来了第五个人就要在外面等待,等到有人出来才能再进去玩。

import random
import time
from multiprocessing import Process, Semaphore def ktv(i, mutex):
with mutex:
print(f"person{i}进来唱歌了")
time.sleep(random.randint(1, 5)) # 唱歌时间
print(f"person{i}从ktv出去了") if __name__ == '__main__':
mutex = Semaphore(4)
for i in range(5):
Process(target=ktv, args=(i, mutex)).start()

输出:

person2进来唱歌了
person0进来唱歌了
person1进来唱歌了
person3进来唱歌了
person1从ktv出去了
person4进来唱歌了
person3从ktv出去了
person0从ktv出去了
person2从ktv出去了
person4从ktv出去了

信号量和锁有点类似,它们之间的区别,信号量,相当于计算器

信号量: 锁 + 计数器

acquire(): 计数器 - 1,计数器减为0时阻塞

release(): 计数器+ 1

4. Condition(条件同步)

Condition被称为条件变量,除了提供与Lock类似的acquire()release()方法外,还提供了wait()notify()方法,还有notify_all()wait_for()方法。

【基本原理】

可以认为Condition对象维护了一个锁(Lock/RLock)和一个waiting池。进程通过acquire()获得Condition对象,当调用wait()方法时,进程会释放Condition内部的锁并进入blocked状态,同时在waiting池中记录这个进程。当调用notify()方法时,Condition对象会从waiting池中挑选一个进程,通知其调用acquire()方法尝试取到锁。

Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个RLock

除了notify()方法外,Condition对象还提供了notify_all()方法,可以通知waiting池中的所有进程尝试acquire内部锁。由于上述机制,处于waiting状态的进程只能通过notify()方法唤醒,所以notify_all()的作用在于防止有的线程永远处于沉默状态。

【示例·一人问一人答】

import time
from multiprocessing import Process, Condition class Male(Process):
def __init__(self, cond):
super().__init__()
self.__cond = cond def run(self):
with self.__cond:
time.sleep(2) # 防止死锁
print("昔日龌龊不足夸")
self.__cond.notify()
self.__cond.wait()
print("春风得意马蹄疾")
self.__cond.notify() class Female(Process):
def __init__(self, cond):
super().__init__()
self.__cond = cond def run(self):
with self.__cond:
self.__cond.wait()
print("今朝放荡思无涯")
self.__cond.notify()
self.__cond.wait()
print("一日看尽长安花") if __name__ == '__main__':
cond = Condition()
male = Male(cond)
female = Female(cond)
female.start()
male.start()

输出:

昔日龌龊不足夸
今朝放荡思无涯
春风得意马蹄疾
一日看尽长安花

5. Event(事件)

Python进程的事件用于主进程控制其他进程的执行,事件对象管理一个内部标志,默认为False,主要提供了以下方法:

  • is_set()

    当且仅当内部标志为True时,返回True

  • set()

    将内部标志设置为True。所有正在等待这个事件的线程将被唤醒。

  • clear()

    将内部标志设置为False

  • wait(timeout=None)

    阻塞进程直到内部变量为True。如果调用时内部标志为True,将立即返回,否则将阻塞进程,直到调用set()方法将标志设置为True或者超过设置的超时时间(秒)。

【示例·模拟红绿灯】

车看作是一个进程,wait()等红灯,根据状态变化,wait()遇到True信号,就非阻塞,遇到False,就阻塞;交通灯也是一个进程 红灯:False 绿灯:True

import random
import time
from multiprocessing import Process, Event class TrafficLight(Process):
def __init__(self, e):
super().__init__(name="TrafficLight")
self.e = e def run(self):
print('\033[1;31m红灯亮\033[0m')
time.sleep(2)
while True:
if not self.e.is_set():
print('\033[1;32m绿灯亮\033[0m')
self.e.set()
else:
print('\033[1;31m红灯亮\033[0m')
self.e.clear()
time.sleep(2) class Car(Process):
def __init__(self, i, e):
super().__init__(name="Car")
self.i = i
self.e = e def run(self):
if not self.e.is_set():
print(f"car{self.i}正在等待")
self.e.wait()
print(f"car{self.i}通过路口") if __name__ == '__main__':
e = Event()
TrafficLight(e).start()
for i in range(50):
time.sleep(random.randrange(0, 5, 2))
Car(i, e).start()

部分输出:

红灯亮
car0正在等待
绿灯亮
car0通过路口
红灯亮
绿灯亮
car1通过路口
红灯亮
car2正在等待
绿灯亮
car2通过路口
红灯亮
car4正在等待
car3正在等待
绿灯亮
car3通过路口
car4通过路口
car5通过路口

6. IPC的解决方案

当使用多个进程时,通常使用消息传递来进行进程之间的通信(IPC),并必须避免使用任何同步原语(如锁)。对于传递消息,可以使用Pipe()(用于两个进程之间的连接)或队列Queue(允许多个生产者和消费者)。

6.1 Pipe(不推荐使用)

Pipe常用来在两个进程间通信,两个进程分别位于管道的两端,默认是双向的。

  • 创建管道

    语法:Pipe(duplex=True)

    功能:在两个进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道

    参数:duplex默认为True,表示管道是全双工的,如果duplexFalse,则conn1只能用于接收,conn2只能用于发送。

    from multiprocessing import Pipe
    
    # 创建双向的Pipe
    conn1, conn2 = Pipe(True)
    # 创建单向Pipe,conn4只能send(),conn3只能recv()
    conn3, conn4 = Pipe(False)
  • conn1.recv()

    接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError

  • conn1.send(obj)

    通过连接发送对象。obj是与序列化兼容的任意对象。

  • conn1.close()

    关闭连接。如果conn1被垃圾回收,将自动调用此方法

  • conn1.poll([timeout])

    如果连接上的数据可用,返回Truetimeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout设为None,操作将无限期地等待数据到达。

【示例】

from multiprocessing import Process, Pipe
import time class Sever(Process):
def __init__(self, sever):
super().__init__()
self.sever = sever def run(self):
n = 0
print("服务器开启...")
while self.sever.poll(0.01):
obj = self.sever.recv()
n += 1
print(f"第{n}行:{obj!r}")
else:
print("服务器关闭...") class Client(Process):
def __init__(self, client):
super().__init__()
self.client = client def run(self):
self.client.send(True)
self.client.send(1)
self.client.send("我自俯察世人,你亦怜惜众生")
self.client.send({"称号": "无极剑圣", "姓名": "易"})
self.client.close() if __name__ == '__main__':
sever, client = Pipe()
Sever(sever).start()
Client(client).start()

输出:

服务器开启...
第1行:True
第2行:1
第3行:'我自俯察世人,你亦怜惜众生'
第4行:{'称号': '无极剑圣', '姓名': '易'}
服务器关闭...

6.2 Queue(推荐使用)

  • 创建队列

    语法:Queue(maxsize=0)

    maxsize是一个整数,用于设置可以放入队列的项目数的上限。达到此大小后,插入将阻止,直到消耗队列项。如果 maxsize小于或等于零,则队列大小为无限大。

  • q.get(block=True, timeout=None)

    返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True。 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中,如果在指定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。

  • q.get_nowait()

    q.get(False)

  • q.put(obj, block=True, timeout=None)

    item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Full异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。

  • q.put_nowait(obj)

    q.put(obj,False)

  • q.qsize()

    返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。

  • q.empty()

    如果调用此方法时 q 为空,返回 True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。

  • q.full()

    如果q已满,返回为True. 由于并发的存在,结果也可能是不可靠的(参考q.empty()方法)。

  • q.close()

    关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常,例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。

  • q.cancel_join_thread()

    不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。

  • q.join_thread()

    连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。

【示例·生产者消费者模型】

import os
import random
import time
from multiprocessing import Process, Queue class Producer(Process):
def __init__(self, q):
super().__init__()
self.queue = q def run(self):
for i in range(10):
time.sleep(random.randint(1, 3))
res = f"包子{i}"
self.queue.put(res)
print(f'\033[1;31m{os.getpid()} 生产了 {res}\033[0m')
self.queue.put(None) class Consumer(Process):
def __init__(self, q):
super().__init__()
self.queue = q def run(self):
while True:
res = self.queue.get()
if res is None: break
time.sleep(random.randint(2, 4))
print(f'\033[1;32m{os.getpid()} 吃了 {res}\033[0m') if __name__ == '__main__':
q = Queue()
Producer(q).start()
Consumer(q).start()

『Python』进程同步的更多相关文章

  1. 『Python』__getattr__()特殊方法

    self的认识 & __getattr__()特殊方法 将字典调用方式改为通过属性查询的一个小class, class Dict(dict): def __init__(self, **kw) ...

  2. 『Python』 ThreadPool 线程池模板

    Python 的 简单多线程实现 用 dummy 模块 一句话就可以搞定,但需要对线程,队列做进一步的操作,最好自己写个线程池类来实现. Code: # coding:utf-8 # version: ...

  3. 『Python』Python 调用 ZoomEye API 批量获取目标网站IP

    #### 20160712 更新 原API的访问方式是以 HTTP 的方式访问的,根据官网最新文档,现在已经修改成 HTTPS 方式,测试可以正常使用API了. 0x 00 前言 ZoomEye 的 ...

  4. 『Python』为什么调用函数会令引用计数+2

    一.问题描述 Python中的垃圾回收是以引用计数为主,分代收集为辅,引用计数的缺陷是循环引用的问题.在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存. sys.g ...

  5. 『Python』库安装

    1.安装指定版本的tensorflow 虽然官网有4种安装方式,并且推荐用anaconda的方式,但是有时候我们需要指定版本的tensorflow,而pip可以做到. 比如我装的是anaconda3. ...

  6. 『Python』装饰器

    一.参考 作者:zhijun liu 链接:https://www.zhihu.com/question/26930016/answer/99243411 来源:知乎 建议大家去原答案浏览 二.装饰器 ...

  7. 『Python』多进程处理

    尝试学习python的多进程模组,对比多线程,大概的区别在: 1.多进程的处理速度更快 2.多进程的各个子进程之间交换数据很不方便 多进程调用方式 进程基本使用multicore() 进程池优化进程的 ...

  8. 『Python』内存分析_list和array

    零.预备知识 在Python中,列表是一个动态的指针数组,而array模块所提供的array对象则是保存相同类型的数值的动态数组.由于array直接保存值,因此它所使用的内存比列表少.列表和array ...

  9. 『Python』VS2015编译源码注意事项

    一.2.5.6版本源码编译 解压 Python-2.5.6.tgz 进入 Pcbuild8 文件夹,使用 vs 2013 打开 pybuild.sln (vs 解决方案),进入 vs2015IDE 环 ...

随机推荐

  1. SpringBoot整合mybatis快速入门

    一.创建一个SpringBoot项目                 二.引入相关依赖 <!--web核心依赖--> <dependency> <groupId>o ...

  2. 异步编程async体会

    namespace 异步编程{ class Program { static void Main(string[] args) { Console.WriteLine("mian this ...

  3. [ASP.NET MVC]@Partial 和@RenderPartial的区别

    @Partial 和@RenderPartial的区别 Html.partial和RenderPartial的用法与区别 Html.partial和RenderPartial都是输出html片段,区别 ...

  4. centos7 添加磁盘到/(根目录下),扩展VG卷和lv

    转载自:http://blog.itpub.net/25469263/viewspace-2662215/ centos7中使用vg方式扩充root分区 1.创建pv fdisk /dev/sdb 输 ...

  5. CSS中定位问题

    通过使用 position 属性,我们可以选择 4 种不同类型的定位,这会影响元素框生成的方式. position 属性值的含义: static 元素框正常生成.块级元素生成一个矩形框,作为文档流的一 ...

  6. Linux centos 安装 jenkins & 本地构建jar & 远程构建jar

    一.部署 jenkins 需要的前奏 1.安装 JDK:https://www.cnblogs.com/chuyi-/p/10644440.html 2.安装tomcat:https://www.cn ...

  7. vmware 配置不同网段双网卡。

    一.前言 需求:由于LVS演练需要,需要配置两张linux OS网卡,而且是不同网段. 准备: 物理机:单网卡 VMware:centos 6.8 二.配置 第一步:新建虚拟机VMware,cento ...

  8. vue项目梳理

    vue实例 vue2.x中创建vue实例,挂载到dom节点 点击查详情 <div id="vue_det"> <h1>site : {{site}}< ...

  9. 改变this指向&闭包特性

    Q:为什么用普通函数时,setTimeout里面的this指向的是window? //通过保留this的方式,通过闭包的特性去拿this let _this = this //...ajax setT ...

  10. 关于ubuntu使用的那些事儿

    时间:2019-04-09 整理:PangYuaner 标题:Ubuntu18.04安装微信(Linux通用) 地址:https://www.cnblogs.com/dotnetcrazy/p/912 ...