《流畅的Python》11-用concurrent.futures (期物)处理并发

期物(future)是指一种对象,表示异步执行的操作。这个概念的作用很大,是 concurrent.futures 模块和asyncio 包(第 18 章讨论)的基础。

期物是译者自创的词,类似于期货,期权,字面上可以简单理解为要执行而未执行的操作。

这一章基本上可以作为协程和asyncio包两个章节中承上启下的部分,因为协程实际上描述了异步的思想和简单实现,而concurrent.futures 模块和asyncio 包(第 18 章讨论)都是多线程的模式,而且现在来说asyncio的用处更广。

自己曾经在写单线程爬虫后了解了一下 Python 多线程,仅限于threading.Thread,关于这类技术用到的时候还是要查阅相关文档和 demo 的,这里还是简单介绍一下思想,以及有哪些操作,不对这个模块进行细究。

先介绍从网络下载批量文件的一般有三种方式,依序,并发(thread_pool),并发(asyncino)。然后主要讲(thread_pool)思路的concurrent.futures模块。

网络下载的三种方式:

  • 依序(sequential
  • 并发(thread_pool) 采用concurrent.future模块
  • 并发(asycino) 采用asycino

并发的两种方法效率差别不大,但都比依序高得多。

concurrent.futures模块

这个模块下有两个类用于用户实现并发:
- ThreadPoolExecutor 多线程方案
- ProcessPoolExecutor 多进程方案

两个类都在内部维护一个工作线程或线程池,抽象层级很高,不需要理会细节实现。 而且 ProcessPoolExecutor 的参数可省,导致第一次看的时候几乎没看出区别,

完成简单任务只需要两行代码。
简单用法(伪代码):

with futures.ThreadPoolExecutor(最大线程数):
  res=executor.map(行为,等待被处理的序列)

.submit()方法 和 .as_complete()函数

上述的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)

concurrent.futures.Future实例的其他用法

  • .done() 不会阻塞线程,返回布尔值,查看期物是否执行
  • .result() 在期物运行结束后调用,会返回可调用对象的结果;未结束的话会阻塞线程,直到这个期物运行结束。
  • add_done_callback() 参数是可调用对象,在期物结束时调用这个可调用对象。

其他相关知识

GIL(Global Interpreter Lock,全局解释器锁)

GIL 一次只允许使用一个线程执行 Python 字节码。因此,一个 Python 进程通常不能同时使用多个 CPU 核心。
然而,标准库中所有执行阻塞型 I/O 操作的函数,在等待操作系统返回结果时都会释放 GIL,从而在 Python 层面,还是可以实现并发的。 所以似乎在这里GIL没有什么用但可以明确的一点是使用concurrent.futures模块可以用来绕过 GIL 做更多的事情.

ProcessPoolExecutor()

前面有提到 ProcessPoolExecutor()其实是多进程的处理方案,他的参数可选,默认为cpu的核心个数。因为在这个案例(网络下载)和多线程的效率相仿,只有简单介绍。在实际运用中,线程个数和进程个数都是要仔细斟酌寻求最优方案。

一个有趣的包 tqdm

提供文本动画进度条,使用方法:

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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值