python编程之进程池、线程池详细梳理

进程/线程的创建过程

进程/线程的创建过程在不同的操作系统有一定的差异,但总体上都是分这三步:创建或调度进程/线程,执行任务,切换或销毁进程/线程。其中创建进程时还需要分配资源;创建线程时不需要分配资源,因为线程使用父进程的资源。

需要注意:创建和销毁进程/线程存在时间开销和内存开销,合理的进程/线程数量可以大大提高运行效率,超过运行环境承受能力的进程/线程数量反而会降低运行效率。而且进程/线程数量越多越容易产生内存泄露问题和内存碎片问题。

进程/线程池的作用

  1. 事先先开好多条进程/线程,有任务时直接使用它们执行任务。
  2. 开好的进程/线程会一直在池中,可被多个任务复用,大大减少开启、关闭、调度进程/线程的时间开销和内存开销。
  3. 池中的线程或进程数量可控,这样可以减少操作系统需要调度的任务个数,达到提高效率减轻负担的效果。
  4. 进程池中进程数量建议设为CPU数量+1个、线程池中线程数量建议设为CPU数量的4倍到5倍。

上述概念可能说的太抽象不好理解,下面假设一个生活中的例子来方便理解:

某服装厂需要生产一万件衣服,不使用池的多任务运行方式相当于招募一万名员工来生产,但由于厂房大小有限且设备数量有限,招募一万名员工生产一万件衣服的方式会大大提高成本降低效率,没有任何工厂会这么干!所以效率最高的方式是服装厂根据厂房大小、设备数量事先招募恰当数量的员工,假设有33台设备的情况招募100名员工,那么平均每名员工生产100件即可完成任务。

这其中的员工数量相当于进程/线程数量,设备数量相当于CPU内核数量,厂房大小相当于内存大小,招募和培训员工相当于创建进程/线程的开销。

进程池/线程池类方法

进程池和线程池的使用方法完全一致,只是它们的类名不同而已,另外要注意区分任务类型,对于IO多计算少的任务使用线程池,对于计算多IO少的任务使用进程池。

concurrent.futures模块是CPython官方提供的进程池/线程池模块,ThreadPoolExecutor类封装了线程池的相关方法、ProcessPoolExecutor类封装了进程池的相关方法。 以下三个是进程池类和线程池类共有的方法,用法完全一致:

submit方法,submit(fn, /, *args, **kwargs):

功能:

调度可调用对象fn,以 fn(*args **kwargs)方式执行并返回Future实例(它可以返回调用对象fn返回的结果)。返回Future类的实例。

参数:

fn:可调用对象。

/:指定后续的args不能以名字传参,只能通过位置传参。

*args:使用args变量接收用户传的所有位置参数。

**kwargs:使用kwargs变量接收用户传的所有关键字参数。

map方法,map(func, *iterables, timeout=None, chunksize=1):

功能:

调度可调用对象func,以 func(遍历iterables中的元素)方式同时执行并返回func的结果到迭代器。返回迭代器,遍历该迭代器可得到所以func的返回值。

参数:

func:指可调用对象。

iterables:指可迭代对象,map会遍历iterables并使用它的每一项元素作为func的参数提交任务。

timeout:指当遍历iterables时延迟了timeout秒后仍然未获取到内容时抛concurrent.futures.TimeoutError异常, timeout可为int或float类型。 若timeout未指定或为None,则不限制等待时间。

chunksize:仅当使用进程池ProcessPoolExecutor时有效,该参数必须是正整数,用来指定将iterables分割成几份提交任务。默认值是1表示不分割,当任务数量很多时分割提交任务可以显著提高性能。该参数是3.5版以后新增的。

shutdown方法,shutdown(wait=True, *, cancel_futures=False):

功能:

用来关闭进程/线程池。

如果使用with语句,你就可以避免显式调用这个方法,它会在全部任务执行完毕或异常退出时自动调用shutdown(wait=True)。

参数:

wait:用来指定何时关闭进程/线程池。若wait为True则在所有任务执行完毕后才关闭进程/线程池,默认值是True;若wait为False则不再接收后续任务,待正在执行的任务完成后立即关闭进程/线程池。

cancel_futures:用来指定是否取消尚未开始的任务。True表示取消;False表示不取消,默认值为False。该参数是3.9版以后新增的。

线程池案例

使用submit方式提交任务:

代码:

from threading import current_thread
import time, random
from concurrent.futures import ThreadPoolExecutor  # 导入线程池类


def func(n):
    print(current_thread().ident, f'任务“{n}”开始运行!')
    time.sleep(random.randint(1, 3))
    print(current_thread().ident, f'任务“{n}”运行完毕。')


if __name__ == '__main__':  # win平台下必须要加,linux和mac平台下可以不加。
    with ThreadPoolExecutor(3) as tp:  # 创建包含3条线程的线程池
        for i in range(9):
            tp.submit(func, i)  # 提交任务

输出:

140519215798016 任务“0”开始运行!
140519207405312 任务“1”开始运行!
140519199012608 任务“2”开始运行!
140519199012608 任务“2”运行完毕。
140519199012608 任务“3”开始运行!
140519215798016 任务“0”运行完毕。
140519215798016 任务“4”开始运行!
140519207405312 任务“1”运行完毕。
140519207405312 任务“5”开始运行!
140519199012608 任务“3”运行完毕。
140519199012608 任务“6”开始运行!
140519215798016 任务“4”运行完毕。
140519215798016 任务“7”开始运行!
140519215798016 任务“7”运行完毕。
140519215798016 任务“8”开始运行!
140519207405312 任务“5”运行完毕。
140519199012608 任务“6”运行完毕。
140519215798016 任务“8”运行完毕。

说明:

  1. ThreadPoolExecutor是一个类,它可以接受一个正整数参数,即指定线程池中线程的数量。

  2. 请注意看运行结果,线程只有3条,当某线程完成当前任务会自带接收下一项任务,直至submit提交的任务全部完成。

  3. 创建线程池建议使用with语句,它会在进入with语句块时创建线程,在退出with语句块时自动关闭线程,这一点非常重要。建议不要用下面这种创建进程池的方式。假设创建的是进程池,由于某些原因强制关闭父进程时:使用下面的方式可能会产生僵尸进程;使用with语句可以减少产生僵尸进程的几率。

    tp = ThreadPoolExecutor(3)
    for i in range(9):
        tp.submit(func, i)
    

使用submit方式提交任务后获取返回值:

错误代码:

from concurrent.futures import ThreadPoolExecutor
import time, random


def pow2(n):
    time.sleep(random.uniform(1, 4))  # 随机休息1至4秒,方便看清运行效果。
    return n, n ** 2


table = [2, 3, 5, 8, 10, 11, 12]  # 任务有7条
with ThreadPoolExecutor(max_workers=3) as e:  # 开启3线程执行任务
    for i in table:  # 遍历table列表提交任务给线程池
        future = e.submit(pow2, i)  # 线程池执行后返回future实例
        print(f'{future.result()[0]}的运算结果是{future.result()[1]}')

说明:

运行上述代码运行时会发现它是排队执行而不是并发执行,因为print语句需要submit语句的运算结果才能执行,所以解释器会等submit语句返回结果将其提交给print语句,这就造成了排队执行而不是并发执行。

正确代码:

from concurrent.futures import ThreadPoolExecutor
import time, random


def pow2(n):
    time.sleep(random.uniform(1, 4))  # 随机休息1至4秒,方便看清多线程运行效果。
    return n, n ** 2


table = [2, 3, 5, 8, 10, 11, 12]  # 任务有7条
table_result = []
with ThreadPoolExecutor(max_workers=3) as e:  # 开启3线程执行任务
    for i in table:  # 遍历table列表提交任务给线程池
        future = e.submit(pow2, i)  # 线程池执行后返回future实例
        table_result.append(future)

for i in table_result:
    print(f'{i.result()[0]}的运算结果是{i.result()[1]}')

说明:

上述代码运行时会并发执行,运行后可以看到程序先是等了几秒,之后打印出所有的运算结果。请注意,这个等待的时间是由线程池中所有任务全部完成需要的时间决定的。

请注意这种方式的缺陷,它是得到所有返回值后一起返回。若我们需要得到一个返回值后立即返回一个值,那么应该用回调,请耐心往下看关于回调的内容。

使用map方式提交任务后获取返回值:

代码:

from concurrent.futures import ThreadPoolExecutor
import time, random


def pow2(n):
    time.sleep(random.uniform(1, 4))  # 随机休息1至4秒,方便看清多线程运行效果。
    return n, n ** 2


table = [2, 3, 5, 8, 10, 11, 12]  # 任务有7条
with ThreadPoolExecutor(max_workers=3) as e:  # 开启3线程执行任务
    ret = e.map(pow2, table)

for i in ret:
    print(f'{i[0]}的运算结果是{i[1]}')

说明:

运行上述代码,你会发现结果和submit完全一致。

map方式与submit方式的对比:

map内部调用了submit,通过对submit封装使用起来更方便。map提交任务返回的是返回值的迭代器,submit提交任务返回的是Future类的实例。在Future类内部有add_done_callback、cancel, cancelled、 done、exception、 result、running、 set_exception、set_result、set_running_or_notify_cancel这么多方法来对任务进行精细控制与管理。

所以,在只需要提交任务并获得返回值时使用map。在需要提交任务并使用回调等功能的使用submit。

Future类方法

Future类将可调用对象封装为异步执行。Future实例由submit创建,不能直接创建。

cancel方法:

尝试取消调用。若调用正在执行或已结束运行则不能取消,返回False;否则调用会被取消,返回True。

cancelled方法:

如果调用成功取消返回True,否则返回False。

running方法:

如果调用正在执行而且不能被取消那么返回True,否则返回False。

done方法:

如果调用已被取消或正常结束那么返回True,否则返回False。

result方法,result(timeout=None):

获取调用返回的值。如果调用还没完成那么这个方法将等待timeout秒。如果在timeout秒内没有执行完成,会触发concurrent.futures.TimeoutError异常。timeout可以是整数或浮点数,若timeout未指定或为None,那么等待时间就没有限制。另外若futrue在完成前被取消则触发CancelledError异常、若调用引发异常这个方法也会引发同样的异常。

exception方法,exception(timeout=None):

返回由调用引发的异常。如果调用还没完成那么这个方法将等待 timeout 秒。如果在timeout秒内没有执行完成,会触发concurrent.futures.TimeoutError异常。timeout可以是整数或浮点数。如果timeout未指定或为None,那么等待时间就没有限制。另外若futrue在完成前被取消则触发CancelledError异常,若调用正常完成则返回None。

add_done_callback方法,add_done_callback(fn):
这是回调方法,附加可调用fn到future实例。当future实例被取消或完成运行时,将会调用fn,且fn会收到这个future实例作为参数。

回调方法

代码:

from concurrent.futures import ThreadPoolExecutor
import time, random


def pow2(n):
    time.sleep(random.uniform(1, 4))  # 随机休息1至4秒,方便看清多线程运行效果。
    return n, n ** 2


def print_pow2(fut):  # 打印pow2处理结果的回调函数
    print(f'{fut.result()[0]}的运算结果是{fut.result()[1]}')


table = [2, 3, 5, 8, 10, 11, 12]  # 任务有7条
with ThreadPoolExecutor(max_workers=3) as e:  # 开启3线程执行任务
    for i in table:  # 遍历table列表提交任务给线程池
        future = e.submit(pow2, i)  # 线程池执行后返回future实例
        future.add_done_callback(print_pow2)

说明:

使用add_done_callback方法绑定回调函数print_pow2,在pow2返回结果后print_pow2立即被调用,print_pow2只能接收一个参数即future实例,使用该实例的result方法可以得到pow2的返回结果。

使用回调方法的好处是多线程并发执行时任何一条线程运行完毕后都可立即用回调函数对它的返回值进行处理。如果不用回调方法那么只能等到全部任务运行结束后才能对返回值进行处理。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值