文章目录
进程和线程
进程和线程
- 对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开两个记事本就启动了两个记事本进程,在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,这个称为线程(Thread)。
- 一个进程至少一个线程。 各线程的执行时间有OS调度,CPU时间片
- 实现多任务的三种方式:
- 多进程模式;(多个进程一起执行多个任务)
- 多线程模式; (一个进程里面多个线程执行多个任务)
- 多进程+多线程模式。 (多个进程的多个线程执行多任务,太复杂,实际少采用)
- 多任务的考虑点:
- 线程切换,单任务型(一个接一个做),多任务切换(每个任务切换着进行看起来像同时进行),多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
- 计算密集型 vs IO 密集型, 计算密集型特点是要进行大量的计算,消耗CPU资源,如视频解码,适合C编写,不适合python这样的脚步语言。 IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度),这种花在CPU上时间少的用脚本语言更合适。
- 异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。对于Python,单线程的异步编程模型称为协程,可以基于事件驱动编写高效的多任务程序。
多进程(multiprocessing)
- 要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
- 优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程, 早期的Apache采用的这种模式。
- 缺点是CPU和内存的开销非常大
fork()
-
only in Linux/Mac/Unix,系统调用,每调用一次返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),子进程永远返回
0
,而父进程返回子进程的ID。理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()
就可以拿到父进程的ID。 -
有了
fork
调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。- 在Python程序中轻松创建子进程:
import os print('Process (%s) start...' % os.getpid()) # Only works on Unix/Linux/Mac: pid = os.fork() if pid == 0: print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid())) else: print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
运行结果如下:
Process (876) start... I (876) just created a child process (877). I am child process (877) and my parent is 876.
-
multiprocessing
模块就是跨平台版本的多进程模块。
multiprocessing
模块
- 提供了一个
Process
类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',)) # 创建子进程
print('Child process will start.')
p.start() # 启动子进程
p.join() # 等待子进程结束后继续往下运行,通常用于进程间同步
print('Child process end.')
执行结果如下:
Parent process 928.
Child process will start.
Run child process test (929)...
Process end.
Pool
如果要启动大量的子进程,可以用进程池的方式批量创建子进程:
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
执行结果如下:
Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.
代码解读:
对Pool
对象调用join()
方法会等待所有子进程执行完毕,调用join()
之前必须先调用close()
,调用close()
之后就不能继续添加新的Process
了。
请注意输出的结果,task 0
,1
,2
,3
是立刻执行的,而task 4
要等待前面某个task完成后才执行,这是因为Pool
的默认大小是CPU的核数, 4核CPU即为最多同时执行4个进程。这是Pool
有意设计的限制,并不是操作系统的限制。如果改成p = Pool(5)
就可以同时跑5个进程。
子进程
subprocess
模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
下面的例子演示了如何在Python代码中运行命令nslookup www.python.org
,这和命令行直接运行的效果是一样的:
import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
运行结果:
$ nslookup www.python.org
Server: 192.168.19.4
Address: 192.168.19.4#53
Non-authoritative answer:
www.python.org canonical name = python.map.fastly.net.
Name: python.map.fastly.net
Address: 199.27.79.223
Exit code: 0
如果子进程还需要输入,则可以通过communicate()
方法输入:
import subprocess
print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
上面的代码相当于在命令行执行命令nslookup
,然后手动输入:
set q=mx
python.org
exit
运行结果如下:
$ nslookup
Server: 192.168.19.4
Address: 192.168.19.4#53
Non-authoritative answer:
python.org mail exchanger = 50 mail.python.org.
Authoritative answers can be found from:
mail.python.org internet address = 82.94.164.166
mail.python.org has AAAA address 2001:888:2000:d::a6
Exit code: 0
进程间通信
Process
之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing
模块包装了底层的机制,提供了Queue
、Pipes
等多种方式来交换数据。
我们以Queue
为例,在父进程中创建两个子进程,一个往Queue
里写数据,一个从Queue
里读数据:
from multiprocessing import Process, Queue
import os, time, random
# 写数据进程执行的代码:
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__=='__main__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
pr.terminate()
运行结果如下:
Process to write: 50563
Put A to queue...
Process to read: 50564
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.
在Unix/Linux下,
multiprocessing
模块封装了fork()
调用,使我们不需要关注fork()
的细节。由于Windows没有fork
调用,因此,multiprocessing
需要“模拟”出fork
的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所以,如果multiprocessing
在Windows下调用失败了,要先考虑是不是pickle失败了。
多线程
-
Python的线程是真正的Posix Thread,而不是模拟出来的线程。
-
多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
-
Python的标准库提供了两个模块:
-
_thread
低级模块 -
threading
,高级模块,是对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
。
-
-
启动一个线程就是把一个函数传入并创建
Thread
实例,然后调用start()
开始执行,threading.current_thread()
它永远返回当前线程的实例MainThread
,子线程的名字在创建Thread实例时指定(仅做显示无实际意义)#!/usr/bin/env python3 # -*- coding: UTF-8 -*- import time, threading def loop(): print('The thread %s is running...' % threading.current_thread().name) n = 0 while n < 5: n += 1 print('The thread %s is running >>> %s' %(threading.current_thread().name, n)) time.sleep(1) print('The thread %s ended' % threading.current_thread().name) print('The thread %s is running' % threading.current_thread().name) t = threading.Thread(target=loop, name= 'loopthread') t.start() t.join() print('The thread %s ended' % threading.current_thread().name) >>>meij1/Videos/OdoCSV/t3.py The thread MainThread is running The thread loopthread is running... The thread loopthread is running >>> 1 The thread loopthread is running >>> 2 The thread loopthread is running >>> 3 The thread loopthread is running >>> 4 The thread loopthread is running >>> 5 The thread loopthread ended The thread MainThread ended
Lock机制
-
在多进程中,同一个变量会copy到每个进程中互不影响,但是在多线程中大家共享同一个变量,为避免大家同时更改同一个变量,需要用到lock机制
-
看这个例子
#!/usr/bin/env python3 # -*- coding: utf-8 -*- from ast import arg from cProfile import run import time, threading balance = 0 def deposit(n): global balance balance += n # 先存后取,结果应该为0 balance -= n def run_it(n): for i in range(200000): deposit(n) t1 = threading.Thread(target=run_it, args=(5,)) t2 = threading.Thread(target=run_it, args=(8,)) t1.start() t2.start() t1.join() t2.join() print(balance) >>>meij1/Videos/OdoCSV/t2.py 0 >>>meij1/Videos/OdoCSV/t2.py -8
上面的结果理论上是0,但是只要循环次数足够多也会产生其他结果。
原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算也分几步执行,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。
balance = balance + n
在内存中执行为2步:
1. 计算`balance + n`,存入临时变量中;
2. 将临时变量的值赋给`balance`。
x = balance + n
balance = x
由于x是局部变量,两个线程各自都有自己的x,当代码正常执行时:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
结果 balance = 0
但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8
结果 balance = -8
-
创建一个锁通过
threading.Lock()
来保证同时只能有一个线程拿到lock,使用完后释放, 例子可以改为, 获得的锁的线程用完后必须得释放锁,为避免其他线程等待无果,所以用try...finally
确保锁一定被释放。balance = 0 lock = threading.Lock() def run_thread(n): for i in range(100000): # 先要获取锁: lock.acquire() try: # 放心地改吧: change_it(n) finally: # 改完了一定要释放锁: lock.release()
多核CPU
-
多核CPU可以支持多个线程,正常情况下一个死循环程序可以100%掉一个CPU,那么4核CPU可以被4个死循环线程干掉(哈哈,在用C、C++或Java来改写的死循环确实会),但是在python中再多的死循环线程也只会100%掉一个核。
-
解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
-
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
-
在Python中,可以使用多线程,但不要指望能有效利用多核。可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
LocalThread
-
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
def process_student(name): std = Student(name) # std是局部变量,但是每个函数都要用它,因此必须传进去: do_task_1(std) do_task_2(std) def do_task_1(std): do_subtask_1(std) do_subtask_2(std) def do_task_2(std): do_subtask_2(std) do_subtask_2(std)
-
ThreadLocal
解决了参数在一个线程中各个函数之间互相传递的问题,一个ThreadLocal
变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。
import threading
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
执行结果:
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
-
全局变量
local_school
就是一个ThreadLocal
对象,每个Thread
对它都可以读写student
属性,但互不影响。你可以把local_school
看成全局变量,但每个属性如local_school.student
都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。 -
ThreadLocal
最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
分布式进程
需要模块
- multiprocessing和queue模块
- 使用BaseManager创建分布式管理器
- 使用Queue创建队列,用于多个进程之间的通信
分布式进程原理
- managers子模块支持把多个进程分布到多台机器上
- 可以写一个服务进程作为调度者,将任务分布到其它多个进程中,然后通过网络通信进行管理
- 比如爬取图片:一般一个进程负责抓取图片的地址,将地址放在Queue(容器)队列中
- 另外一个进程负责从Queue队列中取出链接地址进行图片下载和存储到到本地
- 上述爬取图片的过程就可以做成分布式,一台机器负责获取链接,另外一台机器负责下载存储
- 上述问题核心:将Queue队列暴露到网络中,让其他机器可以访问
分布式进程实现步骤
- 建立Queue队列,负责进程之间的通信,任务队列task_queue,结果队列result_queue
- 把第一步中的两个队列在网络中注册,注册时候将队列重新命名
- 创建一个Queuemanager(BaseManager)的实例manager,相当于一个服务器,给定IP地址、端口和验证码
- 启动实例manager
- 访问Queue对象,即创建网络中暴露重命名后的Queue实例
- 创建任务到本地队列中,自动上传任务到网络队列中,分配给任务进程进行处理
- 任务进程先从网络中任务队列中取出任务,然后执行,将执行结果放入到网络中的结果队列中
- 服务进程从结果队列中取出结果,直到执行完所有任务和取出所有的结果,任务进程关闭,然后服务进行关闭
注意
- 先创建服务进程,再创建任务进程
- 执行时,先启动服务进程,在创建任务进程,启动任务进程不要超过服务进程取出结果的等待时间
分布式进程实例
- 创建一个分布式进程,用来完成10次乘法任务
服务进程:
# 服务进程在windows系统和Linux系统上有所不同
# 创建一个分布式进程:包括服务进程和任务进程
# 多个进程之间的通信使用Queue
# 该代码为服务进程
# 注意,运行时先运行服务进程,再运行任务进程
# 任务执行循序:
# 服务进程和任务进行都创建了相同的两个队列,一个用来放任务,一个用来放结果
# 第一步:服务进程运行,比如将数字2放进任务队列,任务进程从任务队列中取出数字2
# 第二步:取出数字,执行任务,就是2*2=4, 任务执行完后,放进结果队列
# 第三步:服务进程从结果队列中,取出结果。
# 第四步:所有任务执行完毕,所有结果都已经取出,最终任务队列和结果队列都是空的了
# -*- coding:utf-8 -*-
import random, queue
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
# 第一步:定义两个Queue队列,一个用于发送任务,一个接收结果
task_queue = queue.Queue()
result_queue = queue.Queue()
# 创建类似的QueueManager,继承BaseManager,用于后面创建管理器
class QueueManager(BaseManager):
pass
# 定义两个函数,返回结果就是Queue队列, 在linux/mac中直接用lambda
def return_task_queue():
global task_queue # 定义成全局变量
return task_queue # 返回发送任务的队列
def return_result_queue():
global result_queue
return result_queue # 返回接收结果的队列
# 第二步:把上面创建的两个队列注册在网络上,利用register方法
# callable参数关联了Queue对象,将Queue对象在网络中暴露
# 在Linux/Mac中用lambda就可以 callable=lambda: task_queue,wins必须用自建的函数,因为lambda不支持序列化
# 第一个参数是注册在网络上队列的名称
def test():
QueueManager.register('get_task_queue', callable=return_task_queue)
QueueManager.register('get_result_queue', callable=return_result_queue)
# 第三步:绑定端口8001,设置验证口令,这个相当于对象的初始化
# 绑定端口并填写验证口令,windows下需要填写IP地址,Linux/Mac下默认为本地,地址可直接为空
manager = QueueManager(address=('127.0.0.1', 8001), authkey=b'abc') # 口令必须写成类似b'abc'形式,只写'abc'运行错误
# 第四步:启动管理器,启动Queue队列,监听信息通道
manager.start()
# 第五步:通过管理实例的方法获访问网络中的Queue对象
# 即通过网络访问获取任务队列和结果队列,创建了两个Queue实例,
task = manager.get_task_queue()
result = manager.get_result_queue()
# 第六步:添加任务,获取返回的结果
# 将任务放到Queue队列中
for i in range(10):
n = random.randint(0, 10) # 返回0到10之间的随机数
print("Put task %s ..." % n)
task.put(n) # 将n放入到任务队列中
# 从结果队列中取出结果
print("Try get results...")
try:
for i in range(10): # #监听结果队列,获取结果数
r = result.get(timeout=5) # 每次等待5秒,取结果队列中的值
print("Result: %s" % r)
except queue.Empty:
print("result queue is empty.")
finally:
# 最后一定要关闭服务,不然会报管道未关闭的错误
manager.shutdown()
print("master exit.")
if __name__ == '__main__':
freeze_support() # Windows下多进程可能出现问题,添加以下代码可以缓解
print("Start!")
test()
任务进程
# coding: utf-8
# 定义具体的任务进程,具体的工作任务是什么
import time, sys, queue
from multiprocessing.managers import BaseManager
# 创建类似的QueueManager,继承BaseManager,用于后面创建管理器
class QueueManager(BaseManager):
pass
# 第一步:使用QueueManager注册用于获取Queue的方法名称
# 前面服务进程已经将队列名称暴露到网络中,
# 该任务进程注册时只需要提供名称即可,与服务进程中队列名称一致
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 第二步:连接到服务器,也就是运行服务进程代码的机器
server_addr = '127.0.0.1'
print("Connet to server %s..." % server_addr)
# 创建一个管理器实例,端口和验证口令保持与服务进程中完全一致
m = QueueManager(address=(server_addr, 8001), authkey=b'abc')
# 连接到网络服务器
m.connect()
# 第三步:从网络上获取Queue对象,并进行本地化,与服务进程是同一个队列
task = m.get_task_queue()
result = m.get_result_queue()
# 第四步:从task队列获取任务,并把结果写入到resul队列
for i in range(10):
try:
# 前面服务进程向task队列中放入了n,这里取出n
# n和n相乘,并将相乘的算式和结果放入到result队列中去
n = task.get(timeout=1) # 每次等待1秒后取出任务
print("run task %d * %d..." % (n, n))
r = '%d * %d = %d' % (n, n, n*n)
time.sleep(1)
result.put(r)
except queue.Empty:
print("task queue is empty.")
# 任务处理结束
print("worker exit.")
启动分布式进程
先运行服务进程,再运行任务进程
运行结果
服务进程运行结果
(py3.9) C:\Users\meij1\Videos\OdoCSV>python t2.py
Start!
Put task 799 into queue
Put task 277 into queue
Put task 709 into queue
Put task 906 into queue
Put task 332 into queue
Put task 608 into queue
Put task 583 into queue
Put task 95 into queue
Put task 806 into queue
Put task 867 into queue
try to get result...
Results: 799 * 799 = 638401
Results: 277 * 277 = 76729
Results: 709 * 709 = 502681
Results: 906 * 906 = 820836
Results: 332 * 332 = 110224
Results: 608 * 608 = 369664
Results: 583 * 583 = 339889
Results: 95 * 95 = 9025
Results: 806 * 806 = 649636
Results: 867 * 867 = 751689
master exit.
任务进程运行结果
(py3.9) C:\Users\meij1\Videos\OdoCSV>python t1.py
Connecting to server 127.0.0.1...
Run task 799 * 799...
Run task 277 * 277...
Run task 709 * 709...
Run task 906 * 906...
Run task 332 * 332...
Run task 608 * 608...
Run task 583 * 583...
Run task 95 * 95...
Run task 806 * 806...
Run task 867 * 867...
Worker exit.
知识补充1
- 当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。然后,在另一台机器上启动任务进程(本机上启动也可以)
知识补充2
- 其中task_queue和result_queue是两个队列,分别存放任务和结果。它们用来进行进程间通信,交换对象。
- 因为是分布式的环境,放入queue中的数据需要等待Workers机器运算处理后再进行读取,
QueueManager.register(‘get_task_queue’, callable=return_task_queue)
QueueManager.register(‘get_result_queue’, callable=return_result_queue)
- 这样就需要对queue用QueueManager进行封装放到网络中,这是通过上面的2行代码来实现的。我们给return_task_queue的网络调用接口取了一个名get_task_queue,而return_result_queue的名字是get_result_queue,方便区分对哪个queue进行操作。task.put(n)即是对task_queue进行写入数据,相当于分配任务。而result.get()即是等待workers机器处理后返回的结果。
知识补充3
-
这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。
-
Queue对象存储在哪?注意到task_worker.py中根本没有创建Queue的代码,所以,Queue对象存储在task_master.py进程中:
-
而Queue之所以能通过网络访问,就是通过QueueManager实现的。由于QueueManager管理的不止一个Queue,所以,要给每个Queue的网络调用接口起个名字,比如get_task_queue。task_worker这里的QueueManager注册的名字必须和task_manager中的一样。对比上面的例子,可以看出Queue对象从另一个进程通过网络传递了过来。只不过这里的传递和网络通信由QueueManager完成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pB1H39gy-1651988964422)(…/resources/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTEzMTgwNzc=,size_16,color_FFFFFF,t_70.png)]
-
authkey有什么用?这是为了保证两台机器正常通信,不被其他机器恶意干扰。如果task_worker.py的authkey和task_master.py的authkey不一致,肯定连接不上。
内容参考
Python分布式进程使用(Queue和BaseManager使用)_Felix-微信(Felixzfb)的博客-CSDN博客