zoukankan      html  css  js  c++  java
  • 流畅的python第十七章使用期物处理并发

    从 Python 3.4 起,标准库中有两个名为 Future 的类:concurrent.futures.Future 和
    asyncio.Future。这两个类的作用相同:两个 Future 类的实例都表示可能已经完成或
    者尚未完成的延迟计算

    我们要记住一件事:通常情况下自己不应该创建期物,而只能由并发框架
    (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 异常。读到 18.1.1 节你会发
    现,asyncio.Future.result 方法不支持设定超时时间,在那个库中获取期物的结果最
    好使用 yield from 结构。不过,对 concurrency.futures.Future 实例不能这么做。

    这两个库中有几个函数会返回期物,其他函数则使用期物,以用户易于理解的方式实现自
    身。使用 17-3 中的 Executor.map 方法属于后者:返回值是一个迭代器,迭代器的
    __next__ 方法调用各个期物的 result 方法,因此我们得到的是各个期物的结果,而非
    期物本身。

    阻塞型I/O和GIL

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

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

    如何在 CPU 密集型作业中使用 concurrent.futures 模块轻松绕开 GIL

    concurrent.futures 模块的文档
    (https://docs.python.org/3/library/concurrent.futures.html)副标题是“Launching parallel
    tasks”(执行并行任务)。这个模块实现的是真正的并行计算,因为它使用
    ProcessPoolExecutor 类把工作分配给多个 Python 进程处理。因此,如果需要做 CPU
    密集型处理,使用这个模块能绕开 GIL,利用所有可用的 CPU 核心。

    ThreadPoolExecutor.__init__ 方法需要 max_workers 参数,指定线程池中线程
    的数量。在 ProcessPoolExecutor 类中,那个参数是可选的,而且大多数情况下不使用
    ——默认值是 os.cpu_count() 函数返回的 CPU 数量。这样处理说得通,因为对 CPU 密
    集型的处理来说,不可能要求使用超过 CPU 数量的职程。而对 I/O 密集型处理来说,可
    以在一个 ThreadPoolExecutor 实例中使用 10 个、100 个或 1000 个线程;最佳线程数
    取决于做的是什么事,以及可用内存有多少,因此要仔细测试才能找到最佳的线程数。

    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('	'*n, n, n))
        sleep(n)
        msg = '{}loiter({}): done.'
        display(msg.format('	'*n, n))
        return n * 10
    
    def main():
        display('Script starting.')
        executor = futures.ThreadPoolExecutor(max_workers=3)
        results = executor.map(loiter, range(5))
        display('result:', results)
        display('Waiting for individual results:')
        for i, result in enumerate(results):
            display('result {}: {}'.format(i, result))
    
    
    main()

    Executor.map 函数易于使用,不过有个特性可能有用,也可能没用,具体情况取决于需
    求:这个函数返回结果的顺序与调用开始的顺序一致。如果第一个调用生成结果用时 10
    秒,而其他调用只用 1 秒,代码会阻塞 10 秒,获取 map 方法返回的生成器产出的第一个
    结果。在此之后,获取后续结果时不会阻塞,因为后续的调用已经结束。如果必须等到获
    取所有结果后再处理,这种行为没问题;不过,通常更可取的方式是,不管提交的顺序,
    只要有结果就获取。为此,要把 Executor.submit 方法和 futures.as_completed 函
    数结合起来使用

    线程和多进程的替代方案
    Python 自 0.9.8 版(1993 年)就支持线程了,concurrent.futures 只不过是使用线程的
    最新方式。Python 3 废弃了原来的 thread 模块,换成了高级的 threading 模块
    (https://docs.python.org/3/library/threading.html)。 如果
    futures.ThreadPoolExecutor 类对某个作业来说不够灵活,可能要使用 threading 模
    块中的组件(如 Thread、Lock、Semaphore 等)自行制定方案,比如说使用 queue 模
    块(https://docs.python.org/3/library/queue.html)创建线程安全的队列,在线程之间传递数
    据。futures.ThreadPoolExecutor 类已经封装了这些组件。
    threading 模块自 Python 1.5.1(1998 年)就已存在,不过有些人仍然继续使用旧的 thread 模块。Python 3 把
    本文档由Linux公社 www.linuxidc.com 整理
    thread 模块重命名为 _thread,以此强调这是低层实现,不应该在应用代码中使用。
    对 CPU 密集型工作来说,要启动多个进程,规避 GIL。创建多个进程最简单的方式是,
    使用 futures.ProcessPoolExecutor 类。不过和前面一样,如果使用场景较复杂,需
    要更高级的工具。multiprocessing 模块
    (https://docs.python.org/3/library/multiprocessing.html)的 API 与 threading 模块相仿,不
    过作业交给多个进程处理。对简单的程序来说,可以用 multiprocessing 模块代替
    threading 模块,少量改动即可。不过,multiprocessing 模块还能解决协作进程遇到
    的最大挑战:在进程之间传递数据。

    总结一下,python中有GIL锁,导致无法和正常使用线程,但是对于IO密集型作业,由于python标准库中的所有堵塞型I/O函数都会释放GIL,允许其他线程运行,所以不妨碍多线程的使用。而对于CPU密集型作业,可以使用concurrent.futures模块绕开GIL。

  • 相关阅读:
    【矩阵】
    关于集合的思路
    TSQL生成Combguid
    ServiceLocator是反模式
    使用表达式树创建对象
    TeamCity配置笔记
    AutoMapper映射ExpressionTree
    Automapper扩展方法
    Cache&Session Viewer
    Orchard源码:Logging
  • 原文地址:https://www.cnblogs.com/lgh344902118/p/8393300.html
Copyright © 2011-2022 走看看