Python多进程,分布式进程的笔记以及os.fork。
更新与2020年11月30日
首先是通过os.fork创建多进程:
官方文档:https://docs.python.org/zh-cn/3/library/multiprocessing.html#module-multiprocessing
参考链接:
https://www.cnblogs.com/jiangfan95/p/11439207.html
https://www.liaoxuefeng.com/wiki/1016959663602400/1017628290184064
https://www.cnblogs.com/Lin-Yi/p/7360855.html
首先重写os.fork的资料。廖大里面写的比较仔细了。
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:
import os
print('Process (%s) start... my parent is (%s)' % (os.getpid(), os.getppid()))
# Only works on Unix/Linux/Mac:
# 新开了一条进程,只能通过pid的返回值来判断哪一条进程执行。如果不加判断,后面的代码全部会执行两遍。
pid = os.fork()
# 子进程的返回值一只是0
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
# 在这里返回的pid就是子进程的pid号码,是否可以在父进程的环境下,再次创建子进程?
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
输出
Process (3473) start... my parent is (730)
I (3473) just created a child process (3475).
I am child process (3475) and my parent is 3473.
从输出来看,Python的主进程还是由自己的上级进程所创建。
参考链接:https://www.jianshu.com/p/54ef8be9e2b1
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os
import sys
import time
import random
sleepTime1 = random.randrange(1, 6)
sleepTime2 = random.randrange(1, 6)
print(f"tpid is {os.getpid()}")
try:
forkPID1 = os.fork() # 创建子进程
except OSError:
sys.exit(1)
if forkPID1 != 0: # 在父进程的情况下
try:
forkPID2 = os.fork() # 再创建一个子进程
except OSError:
sys.exit(1)
if forkPID2 != 0:
print("parent waiting to child process ...")
print("tpid:{pid}, forkPID1:{fpid1},forkPID2:{fpid2}\n".format(pid=os.getpid(),
fpid1=forkPID1, fpid2=forkPID2))
try:
# 默认先等待后启动的进程,也就是最近启动的进程
child1 = os.wait()[0]
except OSError:
sys.exit(1)
print("parend: child{ch1} finshed first, one child left". format(ch1=child1))
# 当开始多个进程,需要等待多个进程结果就需要多个os.wait
try:
child2 = os.wait()[0]
except OSError:
sys.exit(1)
print("parend: child{ch2} finshed first". format(ch2=child2))
else:
print("Child2 sleeping for {d} second...\ntpid:{pid}, forkPID1:{fpid1},forkPID2:{fpid2}\n".format(d=sleepTime2,pid=os.getppid(),
fpid1=forkPID1, fpid2=os.getpid()))
time.sleep(sleepTime2)
else:
print("Child1 sleeping for {d} second...\ntpid:{pid},forkPID1:{fpid1}\n".format(d=sleepTime1,
pid=os.getppid(),fpid1=os.getpid()))
time.sleep(sleepTime2)
输出
tpid is 5050
parent waiting to child process ...
tpid:5050, forkPID1:5052,forkPID2:5053
Child1 sleeping for 5 second...
tpid:5050,forkPID1:5052
Child2 sleeping for 3 second...
tpid:5050, forkPID1:5052,forkPID2:5053
parend: child5053 finshed first, one child left
这个脚本的运行,实现了一个主进程,多次fork子进程的方法,也实现了,等待子进程完成后,主进程退出的方式。
下面是廖大的案例
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 5189.
Child process will start.
Run child process test (5194)...
Child process end.
可以看出两条进程。
下面介绍廖大的通过进程池,异步执行多个任务
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)
# 执行5个进程任务
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 5323.
Waiting for all subprocesses done...
Run task 0 (5330)...
Run task 1 (5328)...
Run task 2 (5329)...
Run task 3 (5327)...
Task 1 runs 0.31 seconds.
Run task 4 (5328)...
Task 4 runs 0.61 seconds.
Task 0 runs 0.94 seconds.
Task 3 runs 0.97 seconds.
Task 2 runs 1.43 seconds.
All subprocesses done.
从输出可以看到,第五个任务也就是4号任务,是在1号任务完成以后,再次启动任务,1号与4号任务的pid都是相同的。
由于Pool的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。
subprocess
参考链接:https://www.cnblogs.com/vamei/archive/2012/09/23/2698014.html
参考链接:https://www.cnblogs.com/security-darren/p/4733368.html
本文介绍了Python subprocess的基本用法,使用 Popen 可以在Python进程中创建子进程,如果只对子进程的执行退出状态感兴趣,可以调用 subprocess.call() 函数,如果想通过异常处理机制解决子进程异常退出的情形,可以考虑使用 subprocess.check_call() 和 subprocess.check_output。如果希望获得子进程的输出,可以调用 subprocess.check_output(),但 Popen() 无疑是功能最强大的。
subprocess模块的缺陷在于默认提供的父子进程间通信手段有限,只有管道;同时创建的子进程专门用来执行外部的程序或命令。
Linux下进程间通信的手段很多,子进程也完全可能从创建之后继续调用
子进程(这里廖大只用了简单的案例,上面的解释还是非常不错的)
很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。
subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
下面的例子演示了如何在Python代码中运行命令nslookup www.python.org,这和命令行直接运行的效果是一样的:
import subprocess
print('$ nslookup www.python.org')
# 直接新建一个子进程跑命令,r为接受的返回状态
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
输出
$ nslookup www.python.org
Server: 192.168.2.1
Address: 192.168.2.1#53
Non-authoritative answer:
www.python.org canonical name = dualstack.python.map.fastly.net.
Name: dualstack.python.map.fastly.net
Address: 151.101.108.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.2.1
Address: 192.168.2.1#53
Non-authoritative answer:
python.org mail exchanger = 50 mail.python.org.
Authoritative answers can be found from:
python.org nameserver = ns-2046.awsdns-63.co.uk.
python.org nameserver = ns-484.awsdns-60.com.
python.org nameserver = ns-981.awsdns-58.net.
python.org nameserver = ns-1134.awsdns-13.org.
mail.python.org internet address = 188.166.95.178
mail.python.org has AAAA address 2a03:b0c0:2:d0::71:1
Exit code: 0
进程间通信
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:
from multiprocessing import Process, Queue
import os, time, random
import multiprocessing
# 生产者
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() * 3)
# 消费者
def read(q):
print("Process to read: %s" % os.getpid())
while True:
value = q.get(True)
print("Get %s from queue." % value)
'''
spawn
The parent process starts a fresh python interpreter process. The child process will only inherit those resources
necessary to run the process object's run() method. In particular, unnecessary file descriptors and handles
from the parent process will not be inherited. Starting a process using this method is rather slow compared
to using fork or forkserver.
可在Unix和Windows上使用。 Windows上的默认设置。
fork
父进程使用 os.fork() 来产生 Python 解释器分叉。子进程在开始时实际上与父进程相同。父进程的所有资源都由子进程继承。
请注意,安全分叉多线程进程是棘手的。
只存在于Unix。Unix中的默认值。
forkserver
程序启动并选择* forkserver * 启动方法时,将启动服务器进程。从那时起,每当需要一个新进程时,父进程就会连接到服务器并请求它分叉一个新进程。
分叉服务器进程是单线程的,因此使用 os.fork() 是安全的。没有不必要的资源被继承。
可在Unix平台上使用,支持通过Unix管道传递文件描述符
'''
if __name__ == '__main__':
multiprocessing.set_start_method("spawn")
q = Queue()
# 生产子进程对象
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
pw.start()
pr.start()
pw.join()
# 消费进程有死循环,需要自我销毁,因为默认都是非守护进程
pr.terminate()
输出
Process to write: 11377
put A to queue...
Process to read: 11378
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失败了。
上面是廖大的官方文档摘录,基本抄了一遍,顺带回顾,巩固了多进程。
分布式进程
稍微看了一下,其实这个用redis也可以完成,但这个集成度很高,应该内置了socket编程以及队列,快速开发部署还是很好的。
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。
Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
举个例子:如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?
原有的Queue可以继续使用,但是,通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了。
我们先看服务进程,服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import random, time, queue
from multiprocessing.managers import BaseManager
# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()
# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass
def task_queue_run():
return task_queue
def result_queue_run():
return result_queue
# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
# 任务接受者也必须注册同样的名字,也会调用这个回调函数。
QueueManager.register('get_task_queue', callable=task_queue_run)
QueueManager.register('get_result_queue', callable=result_queue_run)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('localhost', 5000), authkey=b'abc')
if __name__ == '__main__':
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)
# 关闭:
# time.sleep(100)
manager.shutdown()
print('master exit.')
上一些个人理解,这个感觉master感觉就是一个web后台,注册的两个方法,可以理解为相对uri,各方都可以调用回调函数的注册方法,操作回调函数执行返回的对象。
这个对象是唯一的,如果是普通对象,多进程操作会数据混乱,所以例子中两个回调函数返回了两个队列,一个用于分发任务,一个用于保存任务。
请注意,当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。
然后,在另一台机器上启动任务进程(本机上启动也可以)
import time, sys, queue
from multiprocessing.managers import BaseManager
# 创建类似的QueueManager:
class QueueManager(BaseManager):
pass
# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字(必须与主机的一致):
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
if __name__ == '__main__':
for i in range(10):
try:
n = task.get(timeout=1)
print('run task %d * %d...' % (n, n))
r = '%d * %d = %d' % (n, n, n*n)
time.sleep(0.11)
result.put(r)
except queue.Empty:
print('task queue is empty.')
# 处理结束:
print('worker exit.')
任务进程要通过网络连接到服务进程,所以要指定服务进程的IP。
现在,可以试试分布式进程的工作效果了。先启动task_master.py服务进程:
task_master.py进程发送完任务后,开始等待result队列的结果。现在启动task_worker.py进程:
task_worker.py进程结束,在task_master.py进程中会继续打印出结果:
这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。
Queue对象存储在哪?注意到task_worker.py中根本没有创建Queue的代码,所以,Queue对象存储在task_master.py进程中:
而Queue之所以能通过网络访问,就是通过QueueManager实现的。由于QueueManager管理的不止一个Queue,所以,要给每个Queue的网络调用接口起个名字,比如get_task_queue。
authkey有什么用?这是为了保证两台机器正常通信,不被其他机器恶意干扰。如果task_worker.py的authkey和task_master.py的authkey不一致,肯定连接不上。
我觉的廖大这里的说法,跟我的理解不一样。QueueManager并不会自己由Queue,就跟翻译的字面意思一样,他是管家,也可以理解他是仓库。
你可以在仓库里面放queue,也可以放text,或者另外的对象。QueueManager主要是提供了网络通信以及方法的注册。
小结
Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。
注意Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。
后续将参考
官方文档:https://docs.python.org/zh-cn/3/library/multiprocessing.html#module-multiprocessing
他人文档参考:https://www.cnblogs.com/jiangfan95/p/11439207.html
官方文档:https://docs.python.org/zh-cn/3/library/multiprocessing.html#module-multiprocessing
参考链接:https://www.cnblogs.com/jiangfan95/p/11439207.html
创建进程的类: Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动) 强调: 1. 需要使用关键字的方式来指定参数 2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
复制代码 复制代码 1 group参数未使用,值始终为None 2 target表示调用对象,即子进程要执行的任务 3 args表示调用对象的位置参数元组,args=(1,2,'anne',) 4 kwargs表示调用对象的字典,kwargs={'name':'anne','age':18} 5 name为子进程的名称 复制代码 复制代码
创建并开启进程的两种方法
第一种通过实例化的方式。
import random
import time
import os
from multiprocessing import Process
def demo(name):
time.sleep(random.random())
print(f'进程:{name}正在执行,pid为{os.getpid()}, 父pid为{os.getppid()}')
for i in range(5):
t = Process(target=demo,args=(i,),name=f'进程{i}') # 参数为元祖
print(t.name)
t.start()
print(f'主进程为{os.getpid()}')
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n1.py 进程0 进程1 进程2 进程3 进程4 主进程为44853 进程:4正在执行,pid为44858, 父pid为44853 进程:2正在执行,pid为44856, 父pid为44853 进程:1正在执行,pid为44855, 父pid为44853 进程:0正在执行,pid为44854, 父pid为44853 进程:3正在执行,pid为44857, 父pid为44853 Process finished with exit code 0
第二种通过继承的方法编写多进程。
import random
from multiprocessing import Process
import time
import os
class MyProcess(Process):
def __init__(self,name):
super(MyProcess, self).__init__()
self.name = f'我是进程{name}'
def run(self) -> None:
time.sleep(random.random())
print(f'进程:{self.name}正在执行,pid为{os.getpid()}, 父pid为{os.getppid()}')
for i in range(5):
t = MyProcess('a' + str(i))
print(t.name)
t.start()
print(f'主进程为{os.getpid()}')
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n2.py 我是进程a0 我是进程a1 我是进程a2 我是进程a3 我是进程a4 主进程为44956 进程:我是进程a1正在执行,pid为44958, 父pid为44956 进程:我是进程a4正在执行,pid为44961, 父pid为44956 进程:我是进程a2正在执行,pid为44959, 父pid为44956 进程:我是进程a0正在执行,pid为44957, 父pid为44956 进程:我是进程a3正在执行,pid为44960, 父pid为44956 Process finished with exit code 0
join让子进程产生阻塞,默认join为无限阻塞,但可以传入时间,设置阻塞的超时时间。
import random
from multiprocessing import Process
import time
import os
class MyProcess(Process):
def __init__(self,name):
super(MyProcess, self).__init__()
self.name = f'我是进程{name}'
def run(self) -> None:
time.sleep(random.random())
print(f'进程:{self.name}正在执行,pid为{os.getpid()}, 父pid为{os.getppid()}')
l_process = []
for i in range(5):
t = MyProcess('a' + str(i))
print(t.name)
t.start()
l_process.append(t)
for m in l_process:
m.join() # join可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
print(f'主进程为{os.getpid()}')
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n2.py 我是进程a0 我是进程a1 我是进程a2 我是进程a3 我是进程a4 进程:我是进程a4正在执行,pid为45013, 父pid为45008 进程:我是进程a1正在执行,pid为45010, 父pid为45008 进程:我是进程a0正在执行,pid为45009, 父pid为45008 进程:我是进程a3正在执行,pid为45012, 父pid为45008 进程:我是进程a2正在执行,pid为45011, 父pid为45008 主进程为45008 Process finished with exit code 0
这里上一个daemon守护进程的实例,还有如何在运行的进程中,获取进程的名称:
#主进程代码运行完毕,守护进程就会结束
from multiprocessing import Process
from multiprocessing import current_process
import time
def foo():
print(current_process().name) # 调试的时候,通过current_process().name返回进程的姓名
print(123)
time.sleep(1)
print("end123") # 没有输出
def bar():
print(456)
time.sleep(3)
print("end456")
p1=Process(target=foo,name='foo') # 给予姓名
p2=Process(target=bar)
p1.daemon=True # 默认为False作为非守护进程,设置为True为守护进程。守护进程会在主程序退出之前自动终止。
p1.start()
p2.start()
time.sleep(0.5) # 阻塞主进行0.5秒,要不然,p1关闭了进程保护,都没机会执行任何代码。
print("main-------")
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n3.py foo 123 456 main------- end456 Process finished with exit code 0
终止进程,terminate终止进程。
import multiprocessing
import time
def foo():
print('Starting worker')
time.sleep(1)
print('Finished worker')
t1 = multiprocessing.Process(target=foo)
t1.start()
print(t1,'is_alive',t1.is_alive())
t1.terminate()
# time.sleep(0.2) # 手动设置阻塞也可以,但决定是join好。
print(t1,'is_alive',t1.is_alive())
t1.join() # 终止进程要使用Join等待进程退出,使进程有足够的时间更新对象的状态,可以反应进程状态
print(t1,'is_alive',t1.is_alive())
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n4.py <Process(Process-1, started)> is_alive True <Process(Process-1, started)> is_alive True <Process(Process-1, stopped[SIGTERM])> is_alive False Process finished with exit code 0
多个进程操作同个资源,也会如果不加锁,可能会引发冲突。
import multiprocessing
import time
import random
lock = multiprocessing.Lock()
def work():
print(multiprocessing.current_process().name + '开始正在工作')
time.sleep(random.random())
print(multiprocessing.current_process().name + '已经结束工作')
def farm():
print(multiprocessing.current_process().name + '开始正在工作')
time.sleep(random.random())
print(multiprocessing.current_process().name + '已经结束工作')
for i in range(5):
t1 = multiprocessing.Process(target=work, name=f'工人{i}')
t2 = multiprocessing.Process(target=farm, name=f'农民{i}')
t1.start()
t2.start()
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n5.py 工人0开始正在工作 农民0开始正在工作 工人1开始正在工作 农民1开始正在工作 工人2开始正在工作 农民2开始正在工作 工人3开始正在工作 农民3开始正在工作 农民3已经结束工作 工人4开始正在工作 农民4开始正在工作 工人2已经结束工作 农民0已经结束工作 农民4已经结束工作 农民1已经结束工作 工人0已经结束工作 工人3已经结束工作 工人1已经结束工作 农民2已经结束工作 工人4已经结束工作 Process finished with exit code 0
上面是不加锁的情况下,工作会进行穿插。
import multiprocessing
import time
import random
lock = multiprocessing.Lock()
def work():
with lock: # 用with写,不用怕死锁
print(multiprocessing.current_process().name + '开始正在工作')
time.sleep(random.random())
print(multiprocessing.current_process().name + '已经结束工作')
def farm():
lock.acquire()
try: # 用try写。
print(multiprocessing.current_process().name + '开始正在工作')
time.sleep(random.random())
print(multiprocessing.current_process().name + '已经结束工作')
finally:
lock.release()
for i in range(5):
t1 = multiprocessing.Process(target=work, name=f'工人{i}')
t2 = multiprocessing.Process(target=farm, name=f'农民{i}')
t1.start()
t2.start()
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n5.py 工人0开始正在工作 工人0已经结束工作 农民0开始正在工作 农民0已经结束工作 工人1开始正在工作 工人1已经结束工作 农民1开始正在工作 农民1已经结束工作 工人2开始正在工作 工人2已经结束工作 农民2开始正在工作 农民2已经结束工作 工人3开始正在工作 工人3已经结束工作 农民3开始正在工作 农民3已经结束工作 工人4开始正在工作 工人4已经结束工作 农民4开始正在工作 农民4已经结束工作 Process finished with exit code 0
可以看出来,加了锁的,农民或工人只有一个能工作,另外一个必须休息,另外只有一把锁,虽然这样安全,但效率真的很低。
进程间通信
虽然可以用文件共享数据实现进程间通信,但问题是:
1)效率低(共享数据基于文件,而文件是硬盘上的数据) 2)需要自己加锁处理
因此我们最好找寻一种解决方案能够兼顾:1)效率高(多个进程共享一块内存的数据)2)帮我们处理好锁问题。
这样看来进程间的通讯还是比较多,可以通过socket进行通讯,还可以文件共享数据,最后也是最方便的是队列,管道,其实就是共享内存。
mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
1 队列和管道都是将数据存放于内存中
2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来, 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性
简单的一些使用介绍
import multiprocessing
q = multiprocessing.Queue(maxsize=3)
q.put('1') # 放如元素
print(q.empty()) # 查看容器是否有空间
q.get() # 取出元素
q.get(block=True) # 设置区块,如果为False取不到数据就报错
一个简单的生产者,消费者关系:
import multiprocessing
import time
def producer(q):
for i in ['鸡腿','duck', 'milk', 'egg']:
time.sleep(1)
print('put the',i)
q.put(i)
def consumer(q):
while True:
print('eat:', q.get(timeout=3)) # 3秒取不到报错
q = multiprocessing.Queue()
t1 = multiprocessing.Process(target=producer,args=(q,))
t2 = multiprocessing.Process(target=consumer,args=(q,))
t1.start()
t2.start()
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n6.py
put the 鸡腿
eat: 鸡腿
put the duck
eat: duck
put the milk
eat: milk
put the egg
eat: egg
Process Process-2:
Traceback (most recent call last):
File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/process.py", line 297, in _bootstrap
self.run()
File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/process.py", line 99, in run
self._target(*self._args, **self._kwargs)
File "/Users/shijianzhong/study/multi_processing/n6.py", line 14, in consumer
print('eat:', q.get(timeout=3))
File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/queues.py", line 105, in get
raise Empty
_queue.Empty
Process finished with exit code 0
import multiprocessing
import time
def producer(q):
for i in ['鸡腿','duck', 'milk', 'egg']:
time.sleep(1)
print('put the',i)
q.put(i)
q.put(None)
def consumer(q):
while True:
if q.get() is None: # 加一个信号,停止条件
break
print('eat:', q.get()) # 3秒取不到报错
q = multiprocessing.Queue()
t1 = multiprocessing.Process(target=producer,args=(q,))
t2 = multiprocessing.Process(target=consumer,args=(q,))
t1.start()
t2.start()
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n6.py put the 鸡腿 put the duck eat: duck put the milk put the egg eat: egg Process finished with exit code 0
通过在队列里面放如一个截止信号,可以让生产者知道后,结束进程。
也可以通过JoinableQueue来实现当生产者停止后,消费者也停止,主要原理是通过阻塞队列来实现。
import multiprocessing
import time
import random
def producer(name, q):
for i in ['鸡腿', 'duck', 'milk', 'egg']:
time.sleep(random.random())
print(name, 'put the', i)
q.put(name + ':' + i)
q.join() # 执行到这一步说明,生产加工已经完成,阻塞队列。
def consumer(name, q):
while True:
time.sleep(random.random())
print(name, 'eat:', q.get()) # 取数据
q.task_done() # 取一次数据,向队列产生一次取出的信号。
q = multiprocessing.JoinableQueue()
p1 = multiprocessing.Process(target=producer, args=('xm001', q,))
p2 = multiprocessing.Process(target=producer, args=('xm002', q,))
p3 = multiprocessing.Process(target=producer, args=('xm001', q,))
t1 = multiprocessing.Process(target=consumer, args=('xibai01', q,))
t2 = multiprocessing.Process(target=consumer, args=('xibai02', q,))
t1.daemon = True # 进程守护打开,消费者的停止,主要就是因为主进程的停止而停止。
t2.daemon = True
for i in (p1, p2, p3, t1, t2):
i.start()
p1.join() # 先阻塞生产者的主函数,函数里面再阻塞队列,等两次阻塞都通过以后,执行主函数,主函数执行后,守护的进程退出。
p2.join()
p3.join()
print('main')
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n7.py xm001 put the 鸡腿 xibai02 eat: xm001:鸡腿 xm002 put the 鸡腿 xibai01 eat: xm002:鸡腿 xm002 put the duck xibai01 eat: xm002:duck xm001 put the duck xm001 put the 鸡腿 xibai01 eat: xm001:duck xibai02 eat: xm001:鸡腿 xm001 put the milk xibai02 eat: xm001:milk xm001 put the egg xibai01 eat: xm001:egg xm002 put the milk xibai02 eat: xm002:milk xm001 put the duck xibai01 eat: xm001:duck xm002 put the egg xibai02 eat: xm002:egg xm001 put the milk xibai02 eat: xm001:milk xm001 put the egg xibai01 eat: xm001:egg main Process finished with exit code 0
这是很早以前早以前写的注释,有些地方表达还是有误的,可阻塞的队列,主要是防止消费进程或者生产进程以外退出,防止队列中的信息没有处理完成。
只有当队列中的数量为0才取消阻塞的状态,消费者每次消费都要调用一task_done,没有看源码,底层应该是一个计数器类似的
2. 管道
创建管道的类:
Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道
参数介绍:
dumplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
Queue就是基础Pipe的,这个我就先不写了。
3. 共享数据
展望未来,基于消息传递的并发编程是大势所趋
即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合
通过消息队列交换数据。这样极大地减少了对使用锁定和其他同步手段的需求,
还可以扩展到分布式系统中
进程间通信应该尽量避免使用本节所讲的共享数据的方式
import multiprocessing
def demo(l):
l.pop() # 没有加锁,数据处理还是可以的,安全起见,可以加锁。
lock = multiprocessing.Lock()
arg = multiprocessing.Manager()
l_all = arg.list(range(20)) # 可以做字典,我这里做的列表
print(l_all)
l_arry = []
for i in range(10):
t = multiprocessing.Process(target=demo, args=(l_all,))
t.start()
l_arry.append(t)
for i in l_arry:
i.join() # 必须阻塞,要不然主程序结束了,数据没了。
print(l_all)
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n9.py [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Process finished with exit code 0
控制资源的并发访问,有时候可能需要允许多个工作进程同时访问一个资源,但要限制总数。列如,连接池支持同时连接,但数目可能是固定的,或者一个网络应用可能支持固定数目的并发下载。
这个时候就可以用Semaphore
import random import multiprocessing import time class ActivePool: def __init__(self): self.mgr = multiprocessing.Manager() self.active = self.mgr.list() # 建立一个模型,开虚拟连接池,这里用列表 self.lock = multiprocessing.Lock() def makeActive(self, name): with self.lock: # 为了防止数据错误,全部加锁了 self.active.append(name) def makeInactive(self, name): with self.lock: self.active.remove(name) def __str__(self): with self.lock: return str(self.active) def worker(s , pool): name = multiprocessing.current_process().name with s: pool.makeActive(name) # 通过s来限制进入的线程,进来的线程操作了都是全共享数据列表 print(f'Activating {name} now running {pool}') time.sleep(random.random()) pool.makeInactive(name) if __name__ == '__main__': pool = ActivePool() s = multiprocessing.Semaphore(3) # 初始化以后开始工作 jobs = [multiprocessing.Process(target=worker, name=str(i), args=(s, pool)) for i in range(10)] for i in jobs: i.start() while True: alive = 0 for j in jobs: # 死循环这个工作任务列表里面的任务,查看是否有任务,有任务就打印出来。 if j.is_alive(): alive += 1 j.join(timeout=0.1) print(f'Now running{pool}') # 没有任务的话,就结束这个循环。 if alive == 0: break
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n10.py Activating 0 now running ['0', '1'] Activating 1 now running ['0', '1', '2'] Activating 2 now running ['0', '1', '2'] Activating 3 now running ['0', '1', '3'] Now running['0', '1', '3'] Now running['0', '1', '3'] Now running['0', '1', '3'] Now running['0', '1', '3'] Now running['0', '1', '3'] Now running['0', '1', '3'] Activating 4 now running ['0', '3', '4'] Now running['0', '3', '4'] Activating 5 now running ['3', '4', '5'] Now running['3', '4', '5'] Activating 6 now running ['4', '5', '6'] Now running['4', '5', '6'] Now running['4', '5', '6'] Now running['4', '5', '6'] Now running['4', '5', '6'] Now running['4', '5', '6'] Now running['4', '5', '6'] Now running['4', '5', '6'] Now running['4', '5', '6'] Activating 7 now running ['5', '6', '7'] Activating 8 now running ['6', '7', '8'] Now running['6', '7', '8'] Now running['6', '7', '8'] Activating 9 now running ['7', '8', '9'] Now running['7', '8', '9'] Now running['7', '9'] Now running['7', '9'] Now running['7', '9'] Now running['7'] Now running['7'] Now running['7'] Now running['7'] Now running['7'] Now running[] Process finished with exit code 0
Event类是一种简单的方法,可以在进程之间传递状态信息。事件可以在设置状态和未设置状态之间切换。通过使用一个可选的超时值,时间对象的用户等待其状态从未设置变为设置。
里面就4个参数['clear', 'is_set', 'set', 'wait']
从字面也可以很好的辩识,后面我上一个简单的代码实例。
# _*_coding:utf-8_*_
# !/usr/bin/env python
from multiprocessing import Process, Event
import time, random
def wait_for_event(e):
print('wait_for_event: starting')
e.wait() # 默认e未设置,这里阻塞
print('wait_for_event: e.is_set()->', e.is_set())
def wait_for_event_timeout(e, t):
print('wait_for_event_timeout: starting')
e.wait(t) # 可以设置阻塞时间
print('wait_for_event_timeout: e.is_set()', e.is_set())
if __name__ == '__main__':
e = Event()
w1 = Process(target=wait_for_event, name='block', args=(e,))
w1.start()
w2 = Process(target=wait_for_event_timeout, name='nonblock', args=(e,2)) # 等待2秒后,阻塞将取消。
w2.start()
print('main:waiting before calling Event.set()') #
time.sleep(3)
e.set() # 3秒后,给时间设置。
print('main:event is set')
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n11.py main:waiting before calling Event.set() wait_for_event: starting wait_for_event_timeout: starting wait_for_event_timeout: e.is_set() False main:event is set wait_for_event: e.is_set()-> True Process finished with exit code 0
事件就像一个全局的对象,传入各个进程里面当做信号进行传输。
最后讲一个pool,进程池。
1 Pool([numprocess [,initializer [, initargs]]]):创建进程池
参数介绍:
1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值 2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None 3 initargs:是要传给initializer的参数组
主要方法:
1 p.apply(func [, args [, kwargs]])
在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async()
2 p.apply_async(func [, args [, kwargs]]):
在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,
将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。
3 p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
4 P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用
5.P.map():在功能上等价于内置的map()的结果,只不过各个任务会并行运行。(感觉跟高阶函数map基本一样,只不过这个是平行操作,返回的时候列表,高阶函数map返回的是迭代器)
Multiprocessing.Pool可以提供指定数量的进程供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行它。Pool类用于需要执行的目标很多,而手动限制进程数量又太繁琐时,如果目标少且不用控制进程数量则可以用Process类。
class multiprocessing.pool.Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])
processes: 是要使用的工作进程数。如果进程是None,那么使用返回的数字os.cpu_count()。也就是说根据本地的cpu个数决定,processes小于等于本地的cpu个数;
initializer: 如果initializer是None,那么每一个工作进程在开始的时候会调用initializer(*initargs)。
maxtasksperchild:工作进程退出之前可以完成的任务数,完成后用一个新的工作进程来替代原进程,来让闲置的资源被释放。maxtasksperchild默认是None,意味着只要Pool存在工作进程就会一直存活。
context: 用在制定工作进程启动时的上下文,一般使用 multiprocessing.Pool() 或者一个context对象的Pool()方法来创建一个池,两种方法都适当的设置了context。
from multiprocessing import Process, Pool
import time
import multiprocessing
def func(msg):
print("msg:", msg)
time.sleep(0.1)
return msg
if __name__ == '__main__':
def s_time(func):
def wrap():
t1 = time.perf_counter()
func()
cost_time = time.perf_counter() - t1
print(f'消耗事件{cost_time:0.5f}')
return wrap
@s_time # 写了一个简单的装饰器,测试成勋跑的事件。
def run():
count = multiprocessing.cpu_count()
pool = Pool(processes=count)
res_l = []
for i in range(10):
msg = "hello %d" % (i)
# res = pool.apply_async(func, (msg,)) # 维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
res = pool.apply(func, (msg,))
res_l.append(res) # 同步执行,即执行完一个拿到结果,再去执行另外一个
print("==============================>")
pool.close()
pool.join() # 调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
print(res_l) # 看到的就是最终的结果组成的列表
for i in res_l: # apply是同步的,所以直接得到结果,没有get()方法
# print(i.get())
print(i)
run()
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n13.py msg: hello 0 msg: hello 1 msg: hello 2 msg: hello 3 msg: hello 4 msg: hello 5 msg: hello 6 msg: hello 7 msg: hello 8 msg: hello 9 ==============================> ['hello 0', 'hello 1', 'hello 2', 'hello 3', 'hello 4', 'hello 5', 'hello 6', 'hello 7', 'hello 8', 'hello 9'] hello 0 hello 1 hello 2 hello 3 hello 4 hello 5 hello 6 hello 7 hello 8 hello 9 消耗事件1.08404 Process finished with exit code 0
from multiprocessing import Process, Pool
import time
import multiprocessing
def func(msg):
print("msg:", msg)
time.sleep(0.1)
return msg
if __name__ == '__main__':
def s_time(func):
def wrap():
t1 = time.perf_counter()
func()
cost_time = time.perf_counter() - t1
print(f'消耗事件{cost_time:0.5f}')
return wrap
@s_time # 写了一个简单的装饰器,测试成勋跑的事件。
def run():
count = multiprocessing.cpu_count()
pool = Pool(processes=count)
res_l = []
for i in range(10):
msg = "hello %d" % (i)
res = pool.apply_async(func, (msg,)) # 维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
# res = pool.apply(func, (msg,))
res_l.append(res) # 同步执行,即执行完一个拿到结果,再去执行另外一个
print("==============================>")
pool.close()
pool.join() # 调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
print(res_l) # 看到的就是最终的结果组成的列表
for i in res_l: # apply是同步的,所以直接得到结果,没有get()方法
print(i.get())
# print(i)
run()
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n13.py ==============================> msg: hello 0 msg: hello 1 msg: hello 2 msg: hello 3 msg: hello 4 msg: hello 5 msg: hello 6 msg: hello 7 msg: hello 8 msg: hello 9 [<multiprocessing.pool.ApplyResult object at 0x109f69450>, <multiprocessing.pool.ApplyResult object at 0x109f69590>, <multiprocessing.pool.ApplyResult object at 0x109f696d0>, <multiprocessing.pool.ApplyResult object at 0x109f69810>, <multiprocessing.pool.ApplyResult object at 0x109f69950>, <multiprocessing.pool.ApplyResult object at 0x109f69b10>, <multiprocessing.pool.ApplyResult object at 0x109f69c50>, <multiprocessing.pool.ApplyResult object at 0x109f69d90>, <multiprocessing.pool.ApplyResult object at 0x109f69ed0>, <multiprocessing.pool.ApplyResult object at 0x109f69a50>] hello 0 hello 1 hello 2 hello 3 hello 4 hello 5 hello 6 hello 7 hello 8 hello 9 消耗事件0.17523 Process finished with exit code 0
最后上演一个进程池的回调函数,还真好用,输入函数的返回值,直接传入了回调函数里面。
from multiprocessing import Pool
import requests
import os
import multiprocessing
def get_page(url):
print('<进程%s> get %s' % (os.getpid(), url)) # 获取进程号,打印网址
respone = requests.get(url)
respone.encoding = 'utf-8'
if respone.status_code == 200:
return {'url': url, 'text': respone.text} # 返回网址,网页源码
def pasrse_page(res):
print('<进程%s> parse %s' % (os.getpid(), res['url'])) # 回调函数进程
parse_res = 'url:<%s> size:[%s]\n' % (res['url'], len(res['text']))
with open('db.txt', 'a') as f: # 写入本地文件
f.write(parse_res)
if __name__ == '__main__':
urls = [
'https://www.baidu.com',
'https://www.python.org',
'https://www.openstack.org',
'https://help.github.com/',
'http://www.sina.com.cn/'
]
count = multiprocessing.cpu_count()
p = Pool(count)
res_l = []
for url in urls:
res = p.apply_async(get_page, args=(url,), callback=pasrse_page) # 异步进行操作
res_l.append(res)
p.close() # 关闭池子,不允许新的进程进入
p.join() # 阻塞主进程
# print([res.get() for res in res_l])
print('main')
/usr/local/bin/python3.7 /Users/shijianzhong/study/multi_processing/n14.py <进程48832> get https://www.baidu.com <进程48833> get https://www.python.org <进程48834> get https://www.openstack.org <进程48835> get https://help.github.com/ <进程48836> get http://www.sina.com.cn/ <进程48831> parse https://www.baidu.com <进程48831> parse http://www.sina.com.cn/ <进程48831> parse https://www.openstack.org <进程48831> parse https://www.python.org <进程48831> parse https://help.github.com/ main Process finished with exit code 0
从进程编号可以看出来,有多个进程参与了解析网页,就一个进程负责写入信息。还是非常不错的工作任务关系。

浙公网安备 33010602011771号