Python多进程任务处理之multiprocessing

来源:互联网 发布:python中的不可变类型 编辑:程序博客网 时间:2024/06/05 10:05

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 16977MainProcess 16384'''

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

from multiprocessing import Processimport osdef 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)...EndMainProcess 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-8print('Exit code:', p.returncode)


4. 进程同步

Lock

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

from multiprocessing import Process, Lockdef func(lock):    lock.acquire()   # 加锁    # do mysql query select update ...    lock.release()   # 解锁lock = Lock()# 设置4个进程,执行funcfor 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:Noneproc2 rev: 0proc2 rev: 1proc2 rev: 2proc2 rev: 3proc2 rev: 4.........proc2 rev: 9995proc2 rev: 9996proc2 rev: 9997proc2 rev: 9998proc2 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, Queuedef 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, Queueimport os, time, randomdef 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, Arraydef func(n, a):    n.value = 50    for i in range(len(a)):        a[i] += 10num = 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, Managerdef 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 Pooldef func(x):    return x*xpool = 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 Poolimport multiprocessingimport osimport randomimport timedef 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 Pooldef func(x):    return x*xdef 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