简单说明python concurrent.futures异步

本文深入探讨了使用Python的concurrent.futures模块进行高效网络I/O处理的方法,通过具体示例展示了ThreadPoolExecutor和PeriodPoolExecutor的运用,以及期物在异步操作中的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简单应用

为了高效处理网络 I/O,需要使用并发,因为网络有很高的延迟,所以为了不浪费 CPU 周期去等待,最好在收到网络响应之前做些其他的事。

concurrent.futures 模块的主要特色是 ThreadPoolExecutorProcessPoolExecutor 类,这两个类实现的接口能分别在不同的线程或进程中执行可调用的对象。这两个类在内部维护着一个工作线程或进程池,以及要执行的任务队列。不过,这个接口抽象的层级很高,下面这种简单的案例,无需关心任何实现细节。
concurrent.futures 官方文档:
https://docs.python.org/zh-cn/3/library/concurrent.futures.html

"""
使用 futures.ThreadPoolExecutor 类
实现多线程下载的脚本
"""
import os
import sys
import time
import requests
from concurrent import futures


# 设定 ThreadPoolExecutor 类最多使用几个线程。
MAX_WORKERS = 20
DEST_DIR = 'downloads/'
BASE_URL = 'http://flupy.org/data/flags'
POP20_CC = ('CN IN US ID BR PK NG BD RU '
            'JP MX PH VN ET EG DE IR TR CD FR').split()


def save_flag(img, filename):
    path = os.path.join(DEST_DIR, filename)
    with open(path, 'wb') as fp:
        fp.write(img)


def get_flag(cc):
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    resp = requests.get(url)
    return resp.content


def show(text):
    print(text, end=' ')
    sys.stdout.flush()


def main(download_many):
    t0 = time.time()
    count = download_many(POP20_CC)
    elapsed = time.time() - t0
    msg = '\n{} flags downloaded in {:.2f}s'
    print(msg.format(count, elapsed))


def download_one(cc):
    image = get_flag(cc)
    show(cc)
    save_flag(image, cc.lower() + '.gif')
    return cc


def download_many(cc_list):
	# 设定工作的线程数量:使用允许的最大值(MAX_WORKERS)与要处理的数量之间较小的那个值,以免创建多余的线程。
    workers = min(MAX_WORKERS, len(cc_list))
    # 使用工作的线程数实例化 ThreadPoolExecutor 类;executor.__exit__ 方法会调用executor.shutdown(wait=True) 方法,
    # 它会在所有线程都执行完毕前阻塞线程。
    with futures.ThreadPoolExecutor(workers) as executor:
    	# map 方法的作用与内置的 map 函数类似,不过 download_one 函数会在多个线程中并发调用;
    	# map 方法返回一个生成器,因此可以迭代,获取各个函数返回的值。
        res = executor.map(download_one, sorted(cc_list))
        # 返回获取的结果数量;如果有线程抛出异常,异常会在这里抛出
        # 这与隐式调用 next()函数从迭代器中获取相应的返回值一样。
        return len(list(res))


if __name__ == '__main__':
    main(download_many)


期物

下面介绍‘期物’的概念:期物指一种对象,表示异步执行的操作。这个 概念的作用很大,是 concurrent.futures 模块和 asyncio 包的基础。

期物是 concurrent.futures 模块和 asyncio 包的重要组件,可是,作为这两个库的用户, 我们有时却见不到期物。上面示例 在背后用到了期物,但是我编写的代码没有直接使用。 下面概述期物,还会举一个例子,展示用法。

Python 3.4 起,标准库中有两个名为Future 的 类:concurrent.futures.Futureasyncio.Future。这两个类的作用相同:两个 Future 类的实例都表示可能已经完成或者尚未完成的延迟计算。这与 Twisted 引擎中的 Deferred 类、Tornado 框架中的 Future 类,以 及多个 JavaScript 库中的 Promise 对象类似。

期物封装待完成的操作,可以放入队列,完成的状态可以查询,得到结果(或抛出异常) 后可以获取结果(或异常)。

我们要记住一件事:通常情况下自己不应该创建期物,而只能由并发框架(concurrent. futures 或 asyncio)实例化。原因很简单:期物表示终将发生的事情,而确定某件事会发生的唯一方式是执行的时间已经排定。因此,只有排定把某件事交给 concurrent.futures. Executor 子 类 处 理 时, 才 会 创 建concurrent.futures.Future 实 例。 例 如,Executor. submit() 方法的参数是一个可调用的对象,调用这个方法后会为传入的可调用对象排期, 并返回一个期物。

客户端代码不应该改变期物的状态,并发框架在期物表示的延迟计算结束后会改变期物的 状态,而我们无法控制计算何时结束。

这两种期物都有 .done() 方法,这个方法不阻塞,返回值是布尔值,指明期物链接的可调 用对象是否已经执行。客户端代码通常不会询问期物是否运行结束,而是会等待通知。因 此,两个 Future 类都有 .add_done_callback() 方法:这个方法只有一个参数,类型是可调用的对象,期物运行结束后会调用指定的可调用对象。

此外,还有.result() 方法。在期物运行结束后调用的话,这个方法在两个Future 类 中的作用相同:返回可调用对象的结果,或者重新抛出执行可调用的对象时抛出的异 常。可是,如果期物没有运行结束,result 方法在两个 Future 类中的行为相差很大。对 concurrency.futures.Future 实例来说,调用 f.result() 方法会阻塞调用方所在的线程, 直到有结果可返回。此时,result 方法可以接收可选的 timeout 参数,如果在指定的时 间内期物没有运行完毕,会抛出TimeoutError 异常。asyncio. Future.result 方法不支持设定超时时间,在那个库中获取期物的结果最好使用 yield from 结构。不过,对 concurrency.futures.Future 实例不能这么做。

这两个库中有几个函数会返回期物,其他函数则使用期物,以用户易于理解的方式实现自 身。使用 上面例子中的 Executor.map 方法属于后者:返回值是一个迭代器,迭代器的 __next__ 方法调用各个期物的 result 方法,因此我们得到的是各个期物的结果,而非期物本身。为了从实用的角度理解期物,我们可以使用concurrent.futures.as_completed 函数 (https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.as_completed)重 写 上面示例。这个函数的参数是一个期物列表,返回值是一个迭代器,在期物运行结束后产 出期物。

为了使用futures.as_completed 函 数, 只 需 修 改download_many 函 数, 把 较 抽 象 的 executor.map 调用换成两个 for 循环:一个用于创建并排定期物,另一个用于获取期物的 结果。同时,我们会添加几个 print 调用,显示运行结束前后的期物。修改后的 download_ many 函数如下示例 ,代码行数由 5 变成 17,不过现在我们能一窥神秘的期物了。其他 函数不变,与上面示例 的一样。

def download_many(cc_list):
	cc_list = cc_list[:5] 
	
	# 把 max_workers 硬编码为 3,以便在输出中观察待完成的期物
	with futures.ThreadPoolExecutor(max_workers=3) as executor:
		to_do = [] 
		
		# 按照字母表顺序迭代国家代码,明确表明输出的顺序与输入一致
		for cc in sorted(cc_list):
			#  executor.submit 方法排定可调用对象的执行时间,
			# 然后返回一个期物,表示这个待执行的操作。
			
			future = executor.submit(download_one, cc) 
			# 存储各个期物,后面传给 as_completed 函数
			to_do.append(future) 
			msg = 'Scheduled for {}: {}' 
			
			# 显示一个消息,包含国家代码和对应的期物
			print(msg.format(cc, future)) 
			
		results = [] 
		
		# as_completed 函数在期物运行结束后产出期物
		for future in futures.as_completed(to_do):
		
			# 获取该期物的结果
			res = future.result()
			msg = '{} result: {!r}'
			
			# 显示期物及其结果
			print(msg.format(future, res)) 
			results.append(res) 
			
	return len(results)


注意,在这个示例中调用 future.result() 方法绝不会阻塞,因为 futureas_completed 函数产出。

我们分析了两个版本的使用 concurrent.futures 库实现的下载脚本:使用 ThreadPoolExecutor. map 方法的示例17-3 和使用 futures.as_completed 函数。

严格来说,我们目前测试的并发脚本都不能并行下载。使用 concurrent.futures 库实现的 那两个示例受 GIL(Global Interpreter Lock,全局解释器锁)的限制,而flags_asyncio.py 脚本在单个线程中运行。

阻塞型I/O和GIL

CPython 解释器本身就不是线程安全的,因此有全局解释器锁(GIL),一次只允许使用一 个线程执行 Python 字节码。因此,一个 Python 进程通常不能同时使用多个 CPU 核心。

编写 Python 代码时无法控制 GIL;不过,执行耗时的任务时,可以使用一个内置的函数 或一个使用 C 语言编写的扩展释放 GIL。其实,有个使用 C 语言编写的 Python 库能管理 GIL,自行启动操作系统线程,利用全部可用的 CPU 核心。这样做会极大地增加库代码的 复杂度,因此大多数库的作者都不这么做。

然而,标准库中所有执行阻塞型I/O 操作的函数,在等待操作系统返回结果时都会释放 GIL。这意味着在 Python 语言这个层次上可以使用多线程,而 I/O 密集型 Python 程序能从 中受益:一个 Python 线程等待网络响应时,阻塞型 I/O 函数会释放 GIL,再运行一个线程。 因此 David Beazley 才说:“Python 线程毫无作用。”

Python 标准库中的所有阻塞型I/O 函数都会释放GIL,允许其他线程运行。 time.sleep() 函数也会释放 GIL。因此,尽管有 GILPython 线程还是能在 I/O 密集型应用中发挥作用。

以后的博客中说明如何在 CPU 密集型作业中使用 concurrent.futures 模块轻松绕开 GIL。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值