锁(Lock)
-
案例:
假设有一个多进程的票务系统,多个用户使用它买票:
抢票案例.py:
import json import time from multiprocessing import Process def search(i): with open('ticket.txt',encoding='utf8') as f: ticket = json.load(f) print(f"{i}你好,当前余票是{ticket['count']}张。") def buy_ticket(i): with open('ticket.txt',encoding='utf8') as f: ticket = json.load(f) if ticket['count']>0: ticket['count'] -= 1 print(f'{i}买到票了!') time.sleep(0.1) with open('ticket.txt',encoding='utf8',mode='wt') as f: ticket = json.dump(ticket,f) if __name__ == "__main__": for i in range(1,6): Process(target=search,args=(f'{i}号',)).start() Process(target=buy_ticket,args=(f'{i}号',)).start()
ticket.txt:
{"count": 1}
运行结果:
1号你好,当前余票是1张。 1号买到票了! 2号你好,当前余票是1张。 3号你好,当前余票是1张。 2号买到票了! 3号买到票了! 4号你好,当前余票是1张。 5号你好,当前余票是1张。 4号买到票了! 5号买到票了!
代码说明:
-
上述案例中buy_ticket函数中有time.sleep(0.1)来模拟网络延迟;
-
查询时多进程受理用户查询操作,结果显示还有1张票;
-
买票后将数据写入文件时因网络延迟造成多名用户同时买到1张余票;
-
由此可见多进程写操作会有数据不安全的问题;
-
要解决上面这个问题必须引入锁的概念,即同一时间只能有一个用户继续写操作。
加入lock后的代码:
import json import time from multiprocessing import Process from multiprocessing import Lock def search(i): with open('ticket.txt',encoding='utf8') as f: ticket = json.load(f) print(f"{i}你好,当前余票是{ticket['count']}张。") def buy_ticket(i): with open('ticket.txt',encoding='utf8') as f: ticket = json.load(f) if ticket['count']>0: ticket['count'] -= 1 print(f'{i}买到票了!') time.sleep(0.1) with open('ticket.txt',encoding='utf8',mode='wt') as f: ticket = json.dump(ticket,f) def lock_buy(i,lock): search(i) with lock: buy_ticket(i) if __name__ == "__main__": lock = Lock() for i in range(1,6): Process(target=lock_buy,args=(f'{i}号',lock)).start()
现在将ticket.txt文件内容改为:
{"count": 2}
运行结果:
1号你好,当前余票是2张。 1号买到票了! 2号你好,当前余票是2张。 3号你好,当前余票是2张。 4号你好,当前余票是2张。 5号你好,当前余票是2张。 2号买到票了!
代码说明:
- 查询余票时并发执行,多个用户可以同时查余票数量;
- 买票时顺序执行,先来先得;
- with lock:等价于 lock.acquire() # 上锁 和 lock.release() # 解锁;
- 建议使用with lock不要使用lock.acquire()和release():一是因为简便;二是使用with可以保证释放锁而release()可能会因进程异常退出造成死锁。
-
-
概念总结:
- 加锁可以保证多个进程修改同一块数据时轮流执行修改;
- 加锁牺牲运行速度来保证数据安全;
- 上锁和解锁必须是一对,上锁未解锁会造成死锁。
进程之间通信
进程之间通信(IPC) Inter Process communication分2种:
一、 基于文件,同一台机器内部多个进程之间通信:
-
Queue:
案例:
from multiprocessing import Queue,Process def son(q): q.put('hello') if __name__ =="__main__": q = Queue() Process(target=son,args=(q,)).start() print(q.get())
代码说明:
- Queue类必须是从multiprocessing中导入,而不是内置模块queue,它俩不是一个东西!
- Queue队列存取顺序是先进先出;
- get是阻塞事件,若get次数比put多那么会导致阻塞等待。
详细说明:
要快速准确了解Queue最好还是看源码:
class Queue(queue.Queue[_T]): # FIXME: `ctx` is a circular dependency and it's not actually optional. # It's marked as such to be able to use the generic Queue in __init__.pyi. def __init__(self, maxsize: int = ..., *, ctx: Any = ...) -> None: ... def get(self, block: bool = ..., timeout: Optional[float] = ...) -> _T: ... def put(self, obj: _T, block: bool = ..., timeout: Optional[float] = ...) -> None: ... def qsize(self) -> int: ... def empty(self) -> bool: ... def full(self) -> bool: ... def put_nowait(self, item: _T) -> None: ... def get_nowait(self) -> _T: ... def close(self) -> None: ... def join_thread(self) -> None: ... def cancel_join_thread(self) -> None: ...
-
init方法:实例化方法,参数一maxsize(可选,默认为不限长度)用来指定队列长度,参数二ctx实际上是不可选参数。
-
get方法,从队列中取数据:
- 参数有2个,参数一block(可选,默认为True)用来指定是否阻塞,参数二timeout(可选,默认为None)用来指定超时阀值 ,该方法默认状态会阻塞当前进程直至取出数据。
- 设置block为True且timeout为正浮点数时表示该方法会阻塞当前进程timeout秒,超时后会抛queue.Empty异常。
- 设置block为false时即表示该方法不阻塞,进程会直接取数据,若队列空会抛queue.Empty异常。另外当block为false时timeout参数失效。
-
get_nowait方法,功能等价于get(False)即不阻塞取数据。
-
put方法,将数据放入队列:
- 参数有3个:参数一obj(必选,无默认值)是要存放的数据,参数二block(可选,默认为True)用来指定是否阻塞,参数三timeout(可选,默认为None)用来指定超时阀值 。若给定长队列存放数据遇到队列满时该方法会阻塞当前进程直至存入数据,若给不定长队列存放数据虽不会遇到阻塞问题但存在撑爆内存的可能。
- 设置block为True且timeout为正浮点数时表示该方法会阻塞当前进程timeout秒,超时后会抛queue.Full异常。
- 设置block为false时即表示该方法不阻塞,进程会直接存数据,如果队列满会抛queue.Full异常。另外当block为false时timeout参数失效。
-
put_nowait方法,参数obj为要存放的数据,功能等价于put(obj, False)即不阻塞存数据。
-
empty方法:判断队列是否空,若为空返回True,若不空返回False。
-
full方法:用来判断队列是否满,若已满返回True,若不满返回False。另外不定长队列永远返回False。
-
案例:
from multiprocessing import Queue,Process def put(i,q): print(f'给队列存放了{i}') q.put(i) def get(q): i = q.get() print(f'从队列取出了{i}') if __name__ =="__main__": q = Queue() for i in range(5): Process(target=put,args=(i,q)).start() for i in range(5): Process(target=get,args=(q,)).start()
输出:
给队列存放了0 给队列存放了3 给队列存放了2 给队列存放了1 给队列存放了4 从队列取出了0 从队列取出了3 从队列取出了1 从队列取出了4 从队列取出了2
代码说明:
- 多进程之间可以使用Queue传递数据;
- 通常使用阻塞方式put和get;
- put和get的顺序是先进先出;
- put和get次数一定要一致,假设put五次get六次那么第六次get会一直陷入阻塞状态直至取到数据。
-
Pipe,基于管道的多进程通信方式,功能和Queue类似。前面已经对Queue详细讲述了使用方法,在这就不对Pipe展开细说了,有需要的朋友可以自己查资料。
二、基于网络,同一台机器或多台机器上的多个进程之间通信:
- 第三方工具(消息中间件)redis、rabbitmq、kafka、memcache,前面三个用的多,memcache用的少。基本上学redis和rabbitmq两种就可以了。因为这2项工具内容很多,细说的话会导致文章篇幅太大,以后有时间再单独写一篇。
生产者与消费者模型
-
使用生产者消费者模型的目的:平衡生产数据和消费数据的效率,让整体效率达到最大化。
-
生产者消费者模型常见应用场景:
-
爬虫:将爬取数据和处理数据分开。爬虫进程是生产者,处理进程是消费者。
-
分布式操作(celery):
-
发布任务进程:它是生产者,负责将任务拆分后放入任务池。
-
任务处理进程:它既是消费者,从任务池中获取任务后进行处理;它又是生产者,将处理任务的结果放入处理结果反馈池。
-
处理结果反馈进程:它是消费者,负责从处理结果反馈池获取数据后进行处理再反馈最终结果。
-
-
-
简单案例:
代码:
from multiprocessing import Queue, Process import requests url = {'csdn': 'https://www.csdn.net/', 'cnblogs': 'https://www.cnblogs.com/', 'baidu': 'https://www.baidu.com/', 'toutiao': 'https://www.toutiao.com/'} def put(key,value, q): r = requests.get(value) q.put((key, r.status_code)) def get(q): while ret := q.get(): print(f'网站{ret[0]}内容已获取,状态码是{ret[1]}') if __name__ == "__main__": q = Queue() p_list = [] for key,value in url.items(): p = Process(target=put, args=(key,value, q)) p.start() p_list.append(p) Process(target=get, args=(q,)).start() for i in p_list: i.join() q.put(None)
输出:
网站baidu内容已获取,状态码是200 网站cnblogs内容已获取,状态码是200 网站toutiao内容已获取,状态码是200 网站csdn内容已获取,状态码是200
代码说明:
- 上述代码是个模拟爬虫的案例,给url字典每一项单独开一条进程获取网页;
- 多个生产者对应一个消费者,因为下载有延迟,下载速度慢于处理速度;
- 生产者从网站获取数据,将(网站名称,网站内容)以元组格式放入队列;
- 消费者从队列中获取数据,将其处理后输出到屏幕(实际生产时会将网页处理后保存到文件);
- 生产者和消费者进程全部开启后,join所有生产者,阻塞主进程;
- 所有生产者进程运行完毕后将None存入队列,用来通知消费者生产完毕;
- 消费者以阻塞状态循环从队列中获取数据,若获取到数据不为None时将其做相应处理,若数据为None时退出循环。
- 上述生产者消费者模型是一个异步阻塞的状态,即多个生产者多进程互不干扰地下载数据(这是异步),一个消费者以阻塞状态从队列中获取数据(这是阻塞)。