多任务(1):多进程

代码环境: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('主进程结束。')

上面代码中,子进程p2exitcodeNone,这是因为p2没有调用join方法,进程还没终止,主进程就结束了。使用时候要特别注意。

进程池

如果要启动大量子进程,我们可以使用进程池multiprocessing.Pool,有这些好处:

  1. 方便控制并发度;
  2. 减少资源消耗,对可重用的资源自动回收利用。

我们稍微修改下上面的例子,用进程池来实现:

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的例子中,我们可能会有这种疑问:

  1. Queue中取数据,为什么不能加个判断终止循环?
  2. 进程池中的apply_async更适合并发,为什么要用apply

Queue有几个方法empty/qsize/full可以判断队列的状态,但是在 python 官方文档中也提到了,这几个方法在多进程或多线程这种复杂状态下是“不可靠”的。

在上面例子中,如果我们加了判断终止读进程循环,并发状态下,写进程time.sleep()了一些时间,在还没写入下一个数据的时候,读进程可能认为队列中已经没数据了,直接退出了读进程。

所以,为了妥协这种情况,我们只能先让写进程跑完,再让读进程去读数据。工作中也是要根据实际业务逻辑具体分析。

进程间共享内存

python 中默认进程间内存隔离,但一些情况下我们依然希望能在进程间共享状态和数据,这就要用到共享内存。

进程池中,进程间共享内存常用multiprocessing.Manager中的Value/Array/dict这三种对象类型。

先看如何创建这三种对象:

  • Value(typecode, value):入参需指定数据类型对应的字符串标识typecode,赋值value
  • Array(typecode, sequence)arraylist很像,主要注意的是,list中可以有多种数据类型,array只能有一种,由第一个入参指定,所以arraylist更节省内存;
  • 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;但操作共享内存需要自行上锁,否则无法得到期望的结果。

总结

  1. 使用多进程记得使用join方法阻塞主进程;
  2. 优先使用进程池,减少资源消耗;
  3. 进程间通信,multiprocessing.Process模块下使用multiprocessing.Queue,而在multiprocessing.Pool模块下使用multiprocessing.Manager实例中的Queue
  4. multiprocessing.Manager()创建了一个管理器对象SyncManager,在进程池或者跨机器的进程间共享数据都会用到它;
  5. 进程间共享内存,常在进程池中使用multiprocessing.Manager实例的Value/Array/dict这三种对象类型。操作共享内存数据注意要自行上锁。
阅读 363

推荐阅读
python3进阶
用户专栏

1 人关注
9 篇文章
专栏主页