zoukankan      html  css  js  c++  java
  • 【笔记】:concurrent.futures 平行运算

    平行运算

    前言:

      编写Python程序时,我们可能会遭遇性能问题,即使优化了代码,程序也依然有可能运行的很慢,从而无法满足我们对执行速度的要求,目前的计算机,其cpu核心数越来越多,于是,我们可以考虑通过平行计算来提升性能,能不能把代码的总计算量分配到多个独立的任务之中,并在多个CPU核心上面同时运行这些任务呢?

      很遗憾,Python的全局解释器锁(GIL)使得我们没有办法用线程实现真正的平行计算,因此,上面那个想法行不通。另外一种常见的建议,是用C语言把程序中对性能要求较高的那部分代码,改为扩展模块,由于C语言更贴近硬件,所以运行的比Python快,一旦运行速度达到要求,我们自然就不用再考虑平行计算了,C语言扩展也可以启动并并行地运行多个原声线程,从而充分利用CPU的多个内核。Python中的C语言扩展API,有完备的文档可供查阅,这使得它成为解决性能问题的一个好办法。

      但是,用C语言重写代码,是有很大代价的,短小而易读的Python代码,会变成冗长而费解的C代码,在进行这样的移植时,必须进行大量的测试,确保移植过程中没有引入bug。然而问题在于:只把程序中的一小部分迁移到C,通常是不够的。一般来说,Python程序之所以执行得比较慢,并不是某个主要因素单独造成的,而是多个因素联合导致的,所以,要想充分利用C语言的硬件和线程优势,就必须把程序中的大量代码移植到C,而这样做,有大幅增加了测试量和风险。于是,我们应该思考一下:有没有一种更好的方式,只需要使用较少的Python代码,即可有效提升执行速度,并迅速解决复杂的计算问题。

      我们可以试着通过内置的concurrent.futures模块,来利用另外一个名叫multiprocessing的内置模块,从而实现这种需求,该做法会以子进程的形式,平行地运行多个解释器,从而令Python程序能够利用多核心CPU提升执行速度,由于子进程与主解释器相分离,所以它们的全局解释器锁也是相互独立的,每个紫禁城都可以完整地利用一个CPU内核,而且这些自经常,都与主进程之间有着联系,通过这条联系渠道,紫禁城可以接收主进程发过来的指令,并把计算结果返回给主进程

    程序运算:

      编写运算量很大的Python程序,查找两数最大公约数,用三种不同的方式进行对比

     单线程

    代码:

    # 单线程
    import time
    
    def gcd(pair):
        a,b = pair
        low = min(a,b)
        for i in range(low,0,-1):
            if a % i == 0 and b % i == 0:
                return i
    
    numbers = [(89937224,53452411),(97432894,43939284),(95938272,94910833),
               (7398473,47382942),(85938272,90493759)]
    start = time.time()
    results = list(map(gcd,numbers))
    end = time.time()
    print('Took %.3f secondes'%(end-start))

    执行结果:

    Took 22.083 secondes
    

    多线程

    代码:

    # 多线程
    import time
    from concurrent.futures import ThreadPoolExecutor
    
    def gcd(pair):
        a,b = pair
        low = min(a,b)
        for i in range(low,0,-1):
            if a % i == 0 and b % i == 0:
                return i
    
    numbers = [(89937224,53452411),(97432894,43939284),(95938272,94910833),
               (7398473,47382942),(85938272,90493759)]
    start = time.time()
    pool = ThreadPoolExecutor(max_workers=2)
    results = list(pool.map(gcd,numbers))
    end = time.time()
    print('Took %.3f secondes'%(end-start))
    

    执行结果:

    Took 25.338 secondes
    

    注:用多条Python现场来改善上述程序,是没有效果的,因为全局解释器锁(GIL)使得Python无法在多个CPU核心上面平行地运行这些线程。线程启动的时候,是有一定开销的,与线程池进行通信,也会有开销,所以上面这个程序运行的比单线程版本还要满

    多进程(只能linux下运行)

    我们只需要改动一行代码,就可以提升整个程序的速度,把ThreadPoolExecutor换成concurrent.futures模块里的ProcessPoolExecutor,程序的速度就上去了

    代码:

    import time
    from concurrent.futures import ProcessPoolExecutor
    from multiprocessing import cpu_count
    
    def gcd(pair):
        a,b = pair
        low = min(a,b)
        for i in range(low,0,-1):
            if a % i == 0 and b % i == 0:
                return i
    
    numbers = [(89937224,53452411),(97432894,43939284),(95938272,94910833),
               (7398473,47382942),(85938272,90493759)]
    start = time.time()
    pool = ProcessPoolExecutor(max_workers=cpu_count())  # 四核
    results = list(pool.map(gcd,numbers))
    end = time.time()
    print('Took %.3f secondes'%(end-start))
    

    执行结果:

    Took 6.816 secondes
    

    注:果然比前面两个版本的程序执行速度快了很多

    总结:

    ProcessPoolExecutor类利用由multiprocessing模块所提供的底层机制,来逐步完成下列操作:

    • 把numbers列表中的每一项输入数据都传给map。
    • 用pickle模块对数据进行序列化,将其变成二进制形式。
    • 通过本地套接字(local socket),将序列化之后的数据从主解释器所在的进程,发到子解释器所在的进程。
    • 接下来,在子进程中,用pickle对二进制数据进行反序列化操作,将其还原为Python对象。
    • 引入包含gcd函数的那个Python模块。
    • 各条子进程平行地针对各自的输入数据,来运行gcd函数。
    • 对运行结果进行序列化操作,将其转变为字节。
    • 将这些字节通过socket负责到主进程之中。
    • 主进程对这些字节执行反序列话操作,将其还原为Python对象。
    • 最后,把每条子进程所求出的计算结果合并到一份列表之中,并返回给调用者

      从编程者的角度看,上面的这些步骤,似乎是比较简单的,但实际上,为了实现平行计算,mutiprocessing模块和ProcessPoolExecutor类在幕后做了大量的工作,如果改用其他编程语言来写,那么开发者只需要用一把同步锁或一项原子操作,就可以把线程之间的同学过程协调好,而在Python语言中,我们却必须使用开销较高的multiprocessing模块,mutiproocessing的开销之所以比较大,原因在于:主进程和子进程之间,必须进行序列化和反序列化操作,而程序中的大量开销,正式由这些操作所引发的。

      对于某些较为孤立,且数据利用率较高的任务来说,这套方案非常合适。所谓孤立,是指待运行的函数不需要与程序中的其他部分共享状态。所谓利用率高,是指只需要在主进程和紫禁城之间传递一部分数据,就能完成大量的运算。本例中的最大公约数算法,满足这两个条件,其他的一些类似数学算法,也可以通过这套方案实现平行计算。

      

  • 相关阅读:
    FlashSocke 通过flash进行socket通信(as代码)
    JavaScript 中的对象深度复制(Object Deep Clone)
    map,vector 等容器内容的循环删除问题(C++)
    [转]用JavaScript在浏览器中创建下载文件
    [记]WIndow/Linux 获取本机(全部)IPv4、IPv6、MAC地址方法 (C/C++)
    [记]Debian alias 设置, 不设置貌似有点不方便习惯
    Linux 安装配置 FTP 服务 (vsftpd)
    FreeSWITCH 安装配置的 各种坑, 填坑
    ubuntu编译安装ruby1.9.3,从p551降级到p484
    redmine3.3.3 rake db:migrate 报错invalid byte sequence in US-ASCII (Argument Error) 解决方法
  • 原文地址:https://www.cnblogs.com/lianzhilei/p/7278956.html
Copyright © 2011-2022 走看看