期物(future
)是指一种对象,表示异步执行的操作。这个概念的作用很大,是 concurrent.futures
模块和asyncio
包(第 18 章讨论)的基础。
期物是译者自创的词,类似于期货,期权,字面上可以简单理解为要执行而未执行的操作。
这一章基本上可以作为协程和asyncio包
两个章节中承上启下的部分,因为协程实际上描述了异步的思想和简单实现,而concurrent.futures
模块和asyncio
包(第 18 章讨论)都是多线程的模式,而且现在来说asyncio
的用处更广。
自己曾经在写单线程爬虫后了解了一下 Python 多线程,仅限于threading.Thread
,关于这类技术用到的时候还是要查阅相关文档和 demo 的,这里还是简单介绍一下思想,以及有哪些操作,不对这个模块进行细究。
先介绍从网络下载批量文件的一般有三种方式,依序,并发(thread_pool),并发(asyncino)。然后主要讲(thread_pool)思路的concurrent.futures
模块。
concurrent.futures
模块 sequential
)thread_pool
) 采用concurrent.future
模块asycino
) 采用asycino
包并发的两种方法效率差别不大,但都比依序高得多。
concurrent.futures
模块这个模块下有两个类用于用户实现并发:
- ThreadPoolExecutor 多线程方案
- ProcessPoolExecutor 多进程方案
两个类都在内部维护一个工作线程或线程池,抽象层级很高,不需要理会细节实现。 而且 ProcessPoolExecutor
的参数可省,导致第一次看的时候几乎没看出区别,
完成简单任务只需要两行代码。
简单用法(伪代码):
with futures.ThreadPoolExecutor(最大线程数):
res=executor.map(行为,等待被处理的序列)
上述的executor.map()
可以分为.submit()
方法 和 .as_complete()
函数 两步实现。
.submit()方法
接受函数和单个对象,同时返回单个期物,.as_complete()函数
返回期物运行结束后的期物,是序列。
一个实例:
def download_many(cc_list):
cc_list = cc_list[:5] ➊
with expression as target:
passfutures.ThreadPoolExecutor(max_workers=3) as executor:
to_do = []
for cc in sorted(cc_list): ➌
future = executor.submit(download_one, cc) ➍
to_do.append(future) ➎
msg = 'Scheduled for {}: {}'
print(msg.format(cc, future)) ➏
results = []
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)
GIL 一次只允许使用一个线程执行 Python 字节码。因此,一个 Python 进程通常不能同时使用多个 CPU 核心。
然而,标准库中所有执行阻塞型 I/O 操作的函数,在等待操作系统返回结果时都会释放 GIL,从而在 Python 层面,还是可以实现并发的。 所以似乎在这里GIL没有什么用但可以明确的一点是使用concurrent.futures
模块可以用来绕过 GIL 做更多的事情.
前面有提到 ProcessPoolExecutor()
其实是多进程的处理方案,他的参数可选,默认为cpu的核心个数。因为在这个案例(网络下载)和多线程的效率相仿,只有简单介绍。在实际运用中,线程个数和进程个数都是要仔细斟酌寻求最优方案。
提供文本动画进度条,使用方法:
import time
from tqdm import tqdm
for i in tqdm(range(1000)):
time.sleep(.01)
最后一个简单的实例:
from time import sleep, strftime
from concurrent import futures
def display(*args):
print(strftime('[%H:%M:%S]'), end=' ')
print(*args)
def loiter(n):
msg = '{}loiter({}): doing nothing for {}s...'
display(msg.format('\t' * n, n, n))
sleep(n)
msg = '{}loiter({}): done.'
display(msg.format('\t' * n, n))
return n * 10
def main():
display('Script starting.')
executor = futures.ThreadPoolExecutor(max_workers=3)
results = executor.map(loiter, range(5))
display('results:', results)
display('Waiting for individual results:')
for i, result in enumerate(results):
display('result {}: {}'.format(i, result))
main()
显示结果:
[00:12:07] Script starting.
[00:12:07] loiter(0): doing nothing for 0s...
[00:12:07] loiter(0): done.
[00:12:07] loiter(1): doing nothing for 1s...
[00:12:07] loiter(2): doing nothing for 2s...
[00:12:07] loiter(3): doing nothing for 3s...
[00:12:07] results: <generator object Executor.map.<locals>.result_iterator at 0x7feabff7a200>
[00:12:07] Waiting for individual results:
[00:12:07] result 0: 0
[00:12:08] loiter(1): done.
[00:12:08] loiter(4): doing nothing for 4s...
[00:12:08] result 1: 10
[00:12:09] loiter(2): done.
[00:12:09] result 2: 20
[00:12:10] loiter(3): done.
[00:12:10] result 3: 30
[00:12:12] loiter(4): done.
[00:12:12] result 4: 40