1.网络图片下载示例
下段代码是依序下载的脚本,并没有在开始直接给出使用future处理的并发版本,是因为实现并发下载的脚本时,会重用其中的大部分代码和设置,因此值得分析一下。
import os
import time
import sys
import requests
POP20_CC = ('CN IN US ID BR PK NG BD PU JP' 'MX PH VN ET EG DE IR TR CD FR').split()
BASE_URL = 'http://flupy.org/data/flags'
DEST_DIR = 'downloads/'
def save_flag(img, filename):
path = os.path.jion(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 download_many(cc_list):
for cc in cc_list:
image = get_flag(cc)
show(cc)
save_flag(image, cc.lower()+'.gif')
return len(cc_list)
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))
if __name__ == '__main__':
main(download_many)
上段代码中,导入了requests库,这个库不在标准库当中,因此,依照惯例,在导入标准库的模块之后导入,而且使用一个空行分隔开。之后,先是列出了人口最多的20个国家,获取国旗图像的网站以及保存图像的本地目录。再是进行了一系列函数的定义。save_flag中把img(字节序列)保存在指定目录中,命名为filename。get_flag中指定了国家代码,构建了URL,然后下载图像,返回响应中的二进制内容。show显示字符串,再进行刷新,进行进度查看。download_many是与并行实现比较的关键函数,它按顺序迭代国家代码列表,返回下载国旗的数量。最后。main函数记录并报告download_many函数之后的耗时。
1.1使用concurrent.futures模块下载
concurrent.futures模块的主要特色是TreadPoolExecutor和ProcessPoolExecutor类,这两个类实现的接口能分别在不同的线程或进程中执行可调用的对象。这两个类在内部维护着一个线程池或进程池,以及要执行的任务队列。下面使用该模块进行国旗下载。
from concurrent import futures
from flags import save_flag, get_flag, show, main
MAX_WORKERS = 20
def dowmload_one(cc):
image = get_flag(cc)
show(cc)
show(image, cc.lower() + '.gif')
return cc
def download_many(cc_list):
workers = min(MAX_WORKERS, len(cc_list))
with futures.ThreadPoolExecutor(workers) as executor:
res = executor.map(dowmload_one, sorted(cc_list))
return len(list(res))
if __name__ == '__main__':
main(download_many)
该段代码,重用了flags模块中的几个函数,并且设定了ThreadPoolExecutor最多使用的线程数。此后,制定了下载一个图像的函数,这是在各个线程中执行的函数。在下载多个图像的函数中,显示设定了工作线程的数量,允许使用的最大值与要处理的数量之间较小的那个值,以免创建多余线程。map函数与内置map函数类似,不过download_one函数会在多个线程当中并发调用,map函数会返回一个生成器,因此可以迭代,获取各个函数的返回值。
1.2future在哪里
future是concurrent.futures模块和asyncio包的重要组件。这两个类的作用类似,两个future实例都表示可能已经完成或者尚未完成的延时计算。有一点要注意,我们不能自己创建future,而只能由并发框架实例化。此外,这两种future都有.done()方法和.result()方法,.done()方法不堵塞,而是返回布尔值,指明future衔接的对象是否已经执行。.result()方法,在future运行结束后调用的话,会返回可调用的对象,或者重新抛出执行可调用的对象时抛出的异常。
2.阻塞型I/O和GIL
CPython解释器本身就不是线程安全的,因此有全局解释器锁(GIL),一次只允许使用一个线程执行Python字节码。因此,一个Python进程通常不能使用多个CPU核心。
在标准库中所有执行堵塞型I/O的函数都会在等待操作系统返回结果时释放GIL。这意味着在Python语言这个层次上可以使用多线程,而I/O密集型Python程序也会从中受益,一个Python程序在等待网络响应时,堵塞型I/O函数会释放GIL,在运行另一个线程。
3.使用concurrent.futures模块启动进程
concurrent.futures模块可以实现真正的并行计算,因为它使用ProcessPoolExecutor类把工作分配给多个Python进程处理。因此,要做CPU密集型处理,使用这个模块能绕开GIL,利用所有可用的CPU核心。使用时,将ThreadPoolExecutor变为ProcessPoolExecutor即可。
值的注意的是,与ThreadPoolExecutor不同,ProcessPoolExecutor中,指定线程池线程数量的那个参数是可选的,而且在大多数情况下不使用——默认值为os.cpu_count()函数返回的cpu数量。