代码环境:python3.6
进程是程序执行时的实例,是操作系统进行资源分配和调度的一个独立单位。
线程是进程内的一个实体,最小的执行单位。一个进程可以有多个线程,在资源占用上,进程大于线程。
现代操作系统都支持“多任务”,我们通过多进程或多线程实现多任务相关的编程。
多进程简单示例
常用的多进程编程可通过multiprocessing
这个模块实现,支持跨平台。
from multiprocessing import Process
from os import getpid
# 子进程要执行的代码
def run_proc(name):
print(f'{name}子进程{getpid()}正在运行...')
def process_main():
print(f'当前进程(主进程)id:{getpid()}')
# 创建子进程,传入执行的任务函数和对应的参数元祖
p1 = Process(target=run_proc, args=('p1', ))
p2 = Process(target=run_proc, args=('p2', ))
print('接下来子进程开始运行...')
# 启动进程
p1.start()
p2.start()
# 阻塞主进程,告诉主进程:等我这个子进程运行结束你再往下继续执行
p1.join()
# p2.join()
# exitcode:0代表进程已终止,非0代表进程尚未终止
print(f'p1的进程退出代码:{p1.exitcode}')
print(f'p2的进程退出代码:{p2.exitcode}')
if __name__ == '__main__':
process_main()
print('主进程结束。')
上面代码中,子进程p2
的exitcode
是None
,这是因为p2
没有调用join
方法,进程还没终止,主进程就结束了。使用时候要特别注意。
进程池
如果要启动大量子进程,我们可以使用进程池multiprocessing.Pool
,有这些好处:
- 方便控制并发度;
- 减少资源消耗,对可重用的资源自动回收利用。
我们稍微修改下上面的例子,用进程池来实现:
from multiprocessing import Pool
from os import getpid
import time, random
# 子进程要执行的代码
def run_proc(name):
print(f'{name}子进程{getpid()}正在运行...')
start = time.time()
time.sleep(random.random() * 3)
print(f'{name}子进程运行{time.time() - start:.2f}秒...')
def process_main():
print(f'当前进程(主进程)id:{getpid()}')
# 创建进程池,我们限制最高可执行3个并发进程
po = Pool(3)
for name in ['p1', 'p2', 'p3', 'p4', 'p5']:
# 传入执行的任务函数和对应的参数元祖,开始并发执行
po.apply_async(run_proc, (name, ))
po.close()
# 等待所有子进程执行完毕,必须放在close后面
po.join()
print('所有子进程结束啦...')
if __name__ == '__main__':
process_main()
进程间通信-Queue
单机上,python 进程间常用通信手段是消息队列multiprocessing.Queue
,我们创建两个进程,一个往Queue
里写数据,一个从Queue
里读数据:
from multiprocessing import Process, Queue
from os import getpid
import time, random
# 写数据进程
def process_write(q):
print(f'写进程:{getpid()}')
for value in ['A', 'B', 'C']:
print(f'queue队列写入数据:{value}...')
q.put(value)
time.sleep(random.random())
# 读数据进程
def process_read(q):
print(f'读进程:{getpid()}')
while True:
value = q.get()
print(f'从queue队列获取数据:{value}...')
def process_main():
print(f'当前进程(主进程)id:{getpid()}')
# 主进程创建Queue
q = Queue()
p_w = Process(target=process_write, args=(q, ))
p_r = Process(target=process_read, args=(q, ))
print('接下来子进程开始运行...')
p_w.start()
p_r.start()
p_w.join()
# 读进程while循环没有退出条件,这里强行终止读进程
p_r.terminate()
if __name__ == '__main__':
process_main()
print('主进程结束。')
进程池中的Queue
使用进程池Pool
创建的进程间通信,使用上面的Queue
会报错,需要使用multiprocessing.Manager
实例中的Queue
:
备注:multiprocessing.Manager()
创建了一个管理器对象SyncManager
,在进程池或者跨机器的进程间共享数据都会用到它。
from multiprocessing import Pool, Manager
from os import getpid
import time, random
# 写数据进程
def process_write(q):
print(f'写进程:{getpid()}')
for value in ['A', 'B', 'C']:
print(f'queue队列写入数据:{value}...'))
q.put(value)
time.sleep(random.random())
# 读数据进程
def process_read(q):
print(f'读进程:{getpid()}')
while not q.empty():
value = q.get()
print(f'从queue队列获取数据:{value}...')
def process_main():
print(f'当前进程(主进程)id:{getpid()}')
po = Pool(2)
with Manager() as manager:
q = manager.Queue()
# 阻塞主进程,相当于顺序执行,写进程完全执行完返回之后,再开始读进程
po.apply(process_write, (q, ))
po.apply(process_read, (q, ))
po.close()
po.join()
if __name__ == '__main__':
process_main()
print('主进程结束。')
在上面用到Queue
的例子中,我们可能会有这种疑问:
- 从
Queue
中取数据,为什么不能加个判断终止循环? - 进程池中的
apply_async
更适合并发,为什么要用apply
?
Queue
有几个方法empty
/qsize
/full
可以判断队列的状态,但是在 python 官方文档中也提到了,这几个方法在多进程或多线程这种复杂状态下是“不可靠”的。
在上面例子中,如果我们加了判断终止读进程循环,并发状态下,写进程time.sleep()
了一些时间,在还没写入下一个数据的时候,读进程可能认为队列中已经没数据了,直接退出了读进程。
所以,为了妥协这种情况,我们只能先让写进程跑完,再让读进程去读数据。工作中也是要根据实际业务逻辑具体分析。
进程间共享内存
python 中默认进程间内存隔离,但一些情况下我们依然希望能在进程间共享状态和数据,这就要用到共享内存。
进程池中,进程间共享内存常用multiprocessing.Manager
中的Value
/Array
/dict
这三种对象类型。
先看如何创建这三种对象:
-
Value(typecode, value)
:入参需指定数据类型对应的字符串标识typecode
,赋值value
; -
Array(typecode, sequence)
:array
跟list
很像,主要注意的是,list
中可以有多种数据类型,array
只能有一种,由第一个入参指定,所以array
比list
更节省内存; -
dict(iterable, **kwargs)
:就是 python 内置的Dict
类型。
常用的typecode
标识(字符串)对照表:
举个例子:
from multiprocessing import Pool, Manager
def worker(i, lock, normal_value, shared_value, shared_array, shared_dict):
normal_value += 1
with lock:
shared_value.value += 1
shared_array[0] += 1
shared_dict['count'] += 1
print(
f'子进程[{i}]--normal:{normal_value},shared_v:{shared_value},shared_a:{shared_array},shared_d:{shared_dict}'
)
def process_main():
po = Pool(2)
with Manager() as manager:
lock = manager.Lock()
normal_value = 0
shared_value = manager.Value('i', 0)
shared_array = manager.Array('i', (0, 1, 2))
shared_dict = manager.dict({'count': 0})
for i in range(5):
po.apply_async(worker, (i, lock, normal_value, shared_value,
shared_array, shared_dict))
po.close()
po.join()
if __name__ == '__main__':
process_main()
print('主进程结束。')
在上面例子的worker
函数中,操作normal_value
不需要上锁,因为进程间默认内存是隔离的,所以每个进程都会得到1
;但操作共享内存需要自行上锁,否则无法得到期望的结果。
总结
- 使用多进程记得使用
join
方法阻塞主进程; - 优先使用进程池,减少资源消耗;
- 进程间通信,
multiprocessing.Process
模块下使用multiprocessing.Queue
,而在multiprocessing.Pool
模块下使用multiprocessing.Manager
实例中的Queue
; -
multiprocessing.Manager()
创建了一个管理器对象SyncManager
,在进程池或者跨机器的进程间共享数据都会用到它; - 进程间共享内存,常在进程池中使用
multiprocessing.Manager
实例的Value
/Array
/dict
这三种对象类型。操作共享内存数据注意要自行上锁。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。