Python并发编程(2):多进程任务处理之multiprocessing

1. 为什么使用python多进程?

因为Python使用全局解释器锁(GIL),它会将进程中的线程序列化,导致每个进程中最多同时运行一个线程,也就是Python多线程不能改善程序性能,不能发挥多核CPU并行提高运行速度的目的。而使用多进程则不受限制,所以实际应用中都是推荐多进程的。

如果执行每个子进程消耗的时间非常短(如执行+1操作),那么就不必使用多进程,因为进程的启动关闭也会耗费系统资源。

使用多进程往往是用于处理CPU密集型(科学计算)的任务需求,如果是IO密集型的任务(如文件读取,爬虫等),应该使用多线程处理。

2. multiprocessing常用组件及功能

创建管理进程的模块:

  • Process(用于创建进程)

  • Pool(用于创建管理进程池)

  • Queue(用于进程通信,资源共享)

  • Value,Array(用于进程通信,资源共享)

  • Pipe(用于管道通信)

  • Manager(用于资源共享)

同步子进程的模块:

  • Condition

  • Event(用于实现进程间同步通信)

  • Lock(当多个进程需要访问共享资源时,Lock用于避免访问冲突)

  • RLock

  • Semaphore(用于控制访问共享资源的数量)


3. 进程

3.1 创建子进程Process

一个进程应该用来做什么?它应该保存哪些状态?它的生命周期是什么样的?

  • 一个进程需要处理一些不同的任务,或者处理不同的对象。

  • 创建子进程需要一个函数和相关参数,参数可以是Process(target = func, args = ( ), kwargs = { }, name =“a”),target选定子进程的函数,args传入函数的参数,name用来标识子进程。

  • 控制子进程进入不同阶段的是方法.start( ),.join( ),.is_alive( ),.terminate( ),.exitcode,这些方法只能在创建子进程中执行。

来看一段代码

from multiprocessing import Process, current_process

# 定义一个函数
def func():
    time.sleep(1)
    proc = current_process()
    print(proc.name, proc.pid)

# 创建子进程,选定func为子进程函数,参数为空   
sub_proc = Process(target = func, args = (), name = 'SubProcess')
sub_proc.start()
sub_proc.join()

# 继续执行主进程
proc = current_process()
print(proc.name, proc.pid)

'''
out:

SubProcess 16977
MainProcess 16384
'''

上述是在主进程中创建子进程,然后用.start( )启动子进程,用.join( )等待子进程执行完成,接着继续执行主进程。另一个在主进程中,运行子进程的例子:

from multiprocessing import Process
import os

def run_proc(name):
    # 多个变量的字符串转义,需要把变量放到一个tuple中
    print('Run child process %s (%s)...' % (name, os.getpid()))     

if __name__ == '__main__':
    print('Parent process %s.' % os.getpid())
    # 创建子进程,target是子进程函数,args是子进程函数的参数
    # 参数要写成tuple形式    
    p = Process(target = run_proc, args = ('test',))
    p.start()       
    p.join()      
    print('End')

'''
out:

Parent process 16384.
Run child process test (16978)...
End
MainProcess 16384
'''

3.2 使用subprocess创建并控制子进程

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(err.decode('utf-8'))
print(output.decode('utf-8'))     # 把输出解码成utf-8
print('Exit code:', p.returncode)


4. 进程同步

Lock

锁是为了确保数据一致性,比如读写锁,每个进程给一个变量增加1,如果在一个进程读取但还没有写入时,另外的进程同时读取并写入该值,则最后写入的值是错误的,这时候就需要锁。

from multiprocessing import Process, Lock

def func(lock):
    lock.acquire()   # 加锁
    # do mysql query select update ...
    lock.release()   # 解锁

lock = Lock()

# 设置4个进程,执行func
for i in range(4):
    proc = Process(target = func, args = (lock,))
    proc.start()

Semaphore

Semaphore和Lock稍有不同,Semaphore相当于N把锁,获取其中一把就可以执行了。信号量的总数N在构造时传入,s = Semaphore(N)。和Lock一样,如果信号量为0,则进程堵塞,直到信号大于0。

Pipe

Pipe是在两个进程之间通信的工具,Pipe构造器会返回两个端

conn1, conn2 = Pipe(True)

如果参数为True,则双端口都可接收发送(全双工的);如果参数为空,则前面的端口conn1用于接收,后面的端口conn2用于发送。代码如下:

from multiprocessing import Pipe, Process

# 发送函数
def proc1(pipe):
   for i in range(10000):
       pipe.send(i)

# 接收函数       
def proc2(pipe):
    while True:
        print("proc2 rev:", pipe.recv())

pipe = Pipe()

# 创建进程,用于发送(pipe的第2个参数用于发送,pipe[1]是proc1的参数)
Process(target = proc1, args = (pipe[1],)).start() 

# 创建进程,用于接收(pipe的第1个参数用于接收,pipe[0]是proc2的参数)
Process(target = proc2, args = (pipe[0],)).start()

'''
out:

None
proc2 rev: 0
proc2 rev: 1
proc2 rev: 2
proc2 rev: 3
proc2 rev: 4
...
...
...
proc2 rev: 9995
proc2 rev: 9996
proc2 rev: 9997
proc2 rev: 9998
proc2 rev: 9999

'''

Pipe的每个端口最多同时一个进程读写,否则数据会出各种问题。

队列Queues

multiprocessing.Queue与Queue.Queue非常相似。其API列表如下:

  • qsize()

  • empty()

  • full()

  • put()

  • put_nowait()

  • get()

  • get_nowait()

  • close()

  • join_thread()

  • cancel_join_thread()

当Queue为Queue.Full状态时,再次put()会堵塞;当状态为Queue.Empty时,再次get()也是。当设置了超时参数而超时,put( )或get( )会抛出异常。Queue主要用于多个进程产生和消费,下面展示十个生产者进程,一个消费者进程,共用同一个队列进行同步。

from multiprocessing import Process, Queue

def producer(q):
    print('Process to producer: %s' % os.getpid())
    for i in range(10):
        q.put(i)

def consumer(q):
    print('Process to consumer: %s' % os.getpid())
    while True:
        print("consumer", q.get())

q = Queue()

# 设置十个生产者进程      
for i in range(10):
    process_producer = Process(target = producer, args = (q,))
    process_producer.start()

# 一个消费者进程    
process_consumer = Process(target = consumer, args = (q,))
process_consumer.start()

利用同一个队列,同步读写,下面是代码内容:

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__':
    q = Queue()
    pw = Process(target = write, args = (q,))
    pr = Process(target = read, args = (q,))

    pw.start()
    pr.start()
    pw.join()
    pr.terminate()

Lock、Pipe、Queue 和Pipe需要注意,尽量避免使用Process.terminate来终止程序,否则将会导致很多问题。


5. 进程间数据共享

前一节中, Pipe、Queue都有一定的数据共享功能,但是它们会堵塞进程。而共享内存、Manager这两种数据共享方式不会堵塞进程, 而且都是多进程安全的。

5.1 共享内存Value,Array

共享内存有两个变量: Value, Array,这两个变量内部都实现了锁机制,因此是多进程安全的。用法如下:

from multiprocessing import Process, Value, Array

def func(n, a):
    n.value = 50
    for i in range(len(a)):
        a[i] += 10

num = Value('d', 0.0)
ints = Array('i', range(10))

p = Process(target = func, args = (num, ints))

p.start()
p.join()

print(num.value)
print(ints[:])

'''
Out: 
50.0
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
'''

Value和Array都需要设置其中存放值的数据类型,d是double类型,i是int类型,具体对应关系在Python标准库的sharedctypes模块中查看。

5.2 服务进程Manager

共享内存支持Value和Array两种结构, 这些值在主进程中管理很分散。在Python 中还有一统天下,无所不能的Server process,专门用于数据共享,其支持的类型非常多,比如list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Queue, Value,Array。用法如下:

from multiprocessing import Process, Manager

def func(dct, lst):
    dct[88] = 88
    lst.reverse()

manager = Manager()
dct = manager.dict()
lst = manager.list(range(5, 10))

p = Process(target = func, args = (dct, lst))
p.start()
p.join()

print(dct, '|', lst)

# Out: {88: 88} | [9, 8, 7, 6, 5]

一个Manager对象就是一个服务进程。在多进程程序中,推荐数据共享就用一个manager对象管理。


6. 进程管理

如果有50个任务要执行, 但是CPU只有4核, 你可以创建50个进程来做这个事情,但这样会增加管理成本。

如果你只想创建4个进程,让它们轮流完成任务,不用自己去管理具体进程的创建销毁,那Pool是非常有用的。

6.1 Pool进程池

Pool是能够管理一定数量进程的进程池。当有空闲进程时,Pool利用空闲进程完成任务,直到所有任务完成为止。使用方法如下:

from multiprocessing import Pool

def func(x):
    return x*x

pool = Pool(processes = 4)

print(pool.map(func, range(8))) 

# out: [0, 1, 4, 9, 16, 25, 36, 49],计算0~7自然数的平方

Pool进程池创建4个进程,不管有没有任务,都一直在进程池中等候,等到有数据的时候就开始执行。考虑到切换进程的时间成本,一般进程池进程个数设置为CPU的逻辑核数,最多不超过其200%。

6.2 Pool异步执行

Pool 的 API 列表如下:

  • apply(func[, args[, kwds]])

  • apply_async(func[, args[, kwds[, callback]]])

  • map(func, iterable[, chunksize])

  • map_async(func, iterable[, chunksize[, callback]])

  • imap(func, iterable[, chunksize])

  • imap_unordered(func, iterable[, chunksize])

  • close()

  • terminate()

  • join()

其中,apply_async( )和map_async( )执行之后,异步返回结果。使用方法如下:

  • apply_async( )
from multiprocessing import Pool
import multiprocessing
import os
import random
import time

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)    # 进程池大小为4,即一次随机执行4个task
    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.')

'''
out:

Parent process 17482.
Run task 1 (17863)...
Run task 0 (17862)...
Run task 2 (17864)...
Run task 3 (17865)...
Waiting for all subprocesses done...
Task 1 runs 0.22 seconds.
Run task 4 (17863)...
Task 2 runs 0.44 seconds.
Task 3 runs 0.94 seconds.
Task 0 runs 1.48 seconds.
Task 4 runs 2.21 seconds.
All subprocesses done.
'''
  • map_async( )
from multiprocessing import Pool

def func(x):
    return x*x

def callback(x):
    print(x, 'in callback')

pool = Pool(processes = 4)

result = pool.map_async(func, range(8), 8, callback)

print(result.get(), 'in main')

'''
out:
[0, 1, 4, 9, 16, 25, 36, 49] in callback
[0, 1, 4, 9, 16, 25, 36, 49] in main
'''

有两个值得提到:一个是callback,另外一个是 multiprocessing.pool.AsyncResult,即上文的变量result。

  • callback是在结果返回之前调用的一个函数,这个函数必须只有一个参数,它会首先接收到结果。callback不能有耗时操作,因为它会阻塞主线程。

  • AsyncResult 是获取结果的对象,其API如下:

    • get([timeout])
    • wait([timeout])
    • ready()
    • successful()

get是获取结果,如果设置了timeout时间,超时会抛出multiprocessing.TimeoutError异常;wait是等待执行完成;ready测试是否已经完成;successful是在确定已经ready的情况下,如果执行中没有抛出异常,则成功,如果没有ready就调用该函数,会得到一个AssertionError 异常。

6.3 Pool管理流程

我们看看Pool的执行流程,有三个阶段:

  1. 一个进程池接收很多任务,然后分开执行任务

  2. 不再接收任务

  3. 等所有任务完成了,回家不干了

这就是上面的Pool API方法:close停止接收新的任务,如果还有任务来,就会抛出异常;join是等待所有任务完成,join必须要在close之后调用,否则会抛出异常;terminate非正常终止,当内存不够用时,垃圾回收器调用的就是这个方法。


7. 参考文章

1. Python 学习笔记 多进程 multiprocessing

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值