zoukankan      html  css  js  c++  java
  • Python Map 并行

    Map是一个酷酷的小东西,也是在Python代码轻松引入并行的关键。对此不熟悉的人会认为map是从函数式语言(如Lisp)借鉴来的东西。map是一个函数 - 将另一个函数映射到一个序列上。例如:

    urls = ['http://www.yahoo.com', 'http://www.reddit.com']
    results = map(urllib2.urlopen, urls)

    这段代码在传入序列的每个元素上应用方法urlopen,并将所有结果存入一个列表中。大致与下面这段代码的逻辑相当:

    results = []
    for url in urls: 
        results.append(urllib2.urlopen(url))

    Map会为我们处理在序列上的迭代,应用函数,最后将结果存入一个方便使用的列表。

    这为什么重要呢?因为利用恰当的库,map让并行处理成为小事一桩!

    map-function

    Python标准库中multiprocessing模块,以及极少人知但同样出色的子模块multiprocessing.dummy,提供了map函数的并行版本。

    题外话:这是啥?你从未听说过这名为dummy的mulprocessing模块的线程克隆版本?我也是最近才知道的。在multiprocessing文档页中仅有一句提到这个子模块,而这句话基本可以归结为“哦,是的,存在这样一个东西”。完全低估了这个模块的价值!

    Dummy是multiprocessing模块的精确克隆,唯一的区别是:multiprocessing基于进程工作,而dummy模块使用线程(也就带来了常见的Python限制)。因此,任何东西可套用到一个模块,也就可以套用到另一个模块。在两个模块之间来回切换也就相当容易,当你不太确定一些框架调用是IO密集型还是CPU密集型时,想做探索性质的编程,这一点会让你觉得非常赞!

    开始

    为了访问map函数的并行版本,首先需要导入包含它的模块:

    # 以下两行引入其一即可
    
    from multiprocessing import Pool
    from multiprocessing.dummy import Pool as ThreadPool

    并实例化池对象:

    # 译注:这里其实是以dummy模块为例
    
    pool = ThreadPool()

    这一句代码处理了example2.py中7行的build_worker_pool函数完成的所有事情。如名所示,这句代码会创建一组可用的工作者,启动它们来准备工作,并将它们存入变量中,方便访问。

    pool对象可以有若干参数,但目前,只需关注第一个:进程/线程数量。这个参数用于设置池中的工作者数目。如果留空,默认为机器的CPU核数。

    一般来说,如果为CPU密集型任务使用进程池(multiprocessing pool),更多的核等于更快的速度(但有一些注意事项)。然而,当使用线程池(threading)处理网络密集型任务时,情况就很不一样了,因此最好试验一下池的最佳大小。

    pool = ThreadPool(4) # 将池的大小设置为4

    如果运行了过多的线程,就会浪费时间在线程切换上,而不是做有用的事情,所以可以把玩把玩直到找到最适合任务的线程数量。

    现在池对象创建好了,简单的并行也是弹指之间的事情了,那来重写example2.py吧。

    import urllib2 
    from multiprocessing.dummy import Pool as ThreadPool 
    
    urls = [
      'http://www.python.org', 
      'http://www.python.org/about/',
      'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
      'http://www.python.org/doc/',
      'http://www.python.org/download/',
      'http://www.python.org/getit/',
      'http://www.python.org/community/',
      'https://wiki.python.org/moin/',
      'http://planet.python.org/',
      'https://wiki.python.org/moin/LocalUserGroups',
      'http://www.python.org/psf/',
      'http://docs.python.org/devguide/',
      'http://www.python.org/community/awards/'
      # 等等...
      ]
    
    # 创建一个工作者线程池
    pool = ThreadPool(4) 
    # 在各个线程中打开url,并返回结果
    results = pool.map(urllib2.urlopen, urls)
    #close the pool and wait for the work to finish
    # 关闭线程池,等待工作结束
    pool.close() 
    pool.join()

    看看!真正做事情的代码仅有4行,其中3行只是简单的辅助功能。map调用轻松搞定了之前示例40行代码做的事情!觉得好玩,我对两种方式进行了时间测量,并使用了不同的池大小。

    # 译注:我觉得与串行处理方式对比意义不大,应该和队列的方式进行性能对比
    
    results = [] 
    for url in urls:
      result = urllib2.urlopen(url)
      results.append(result)
    
    # # ------- 对比 ------- # 
    
    
    # # ------- 池的大小为4 ------- # 
    pool = ThreadPool(4) 
    results = pool.map(urllib2.urlopen, urls)
    
    # # ------- 池的大小为8 ------- # 
    
    pool = ThreadPool(8) 
    results = pool.map(urllib2.urlopen, urls)
    
    # # ------- 池的大小为13 ------- # 
    
    pool = ThreadPool(13) 
    results = pool.map(urllib2.urlopen, urls)

    结果:

    单线程: 14.4 秒
    池大小为4时:3.1 秒
    池大小为8时:1.4 秒
    池大小为13时:1.3秒
    

    真是呱呱叫啊!也说明了试验不同的池大小是有必要的。在我的机器上,池的大小大于9后会导致性能退化(译注:咦,结果不是显示13比8的性能要好么?)。

    现实中的Example 2

    为千张图片创建缩略图。

    来做点CPU密集型的事情!对于我,在工作中常见的任务是操作大量的图片目录。其中一种图片转换是创建缩略图。这项工作适于并行处理。

    基本的单进程设置

    from multiprocessing import Pool 
    from PIL import Image
    
    SIZE = (75,75)
    SAVE_DIRECTORY = 'thumbs'
    
    def get_image_paths(folder):
      return (os.path.join(folder, f) 
          for f in os.listdir(folder) 
          if 'jpeg' in f)
    
    def create_thumbnail(filename): 
      im = Image.open(filename)
      im.thumbnail(SIZE, Image.ANTIALIAS)
      base, fname = os.path.split(filename) 
      save_path = os.path.join(base, SAVE_DIRECTORY, fname)
      im.save(save_path)
    
    if __name__ == '__main__':
      folder = os.path.abspath(
        '11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840')
      os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
    
      images = get_image_paths(folder)
    
      for image in images: 
        create_thumbnail(image)

    示例代码中用了一些技巧,但大体上是:向程序传入一个目录,从目录中获取所有图片,然后创建缩略图,并将缩略图存放到各自的目录中。

    在我的机器上,这个程序处理大约6000张图片,花费27.9秒。

    如果使用一个并行的map调用来替换for循环:

    from multiprocessing import Pool 
    from PIL import Image
    
    SIZE = (75,75)
    SAVE_DIRECTORY = 'thumbs'
    
    def get_image_paths(folder):
      return (os.path.join(folder, f) 
          for f in os.listdir(folder) 
          if 'jpeg' in f)
    
    def create_thumbnail(filename): 
      im = Image.open(filename)
      im.thumbnail(SIZE, Image.ANTIALIAS)
      base, fname = os.path.split(filename) 
      save_path = os.path.join(base, SAVE_DIRECTORY, fname)
      im.save(save_path)
    
    if __name__ == '__main__':
      folder = os.path.abspath(
        '11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840')
      os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
    
      images = get_image_paths(folder)
    
      pool = Pool()
      pool.map(create_thumbnail, images)
      pool.close() 
      pool.join()

    5.6秒!

    仅修改几行代码就能得到巨大的速度提升。这个程序的生产环境版本通过切分CPU密集型工作和IO密集型工作并分配到各自的进程和线程(通常是死锁代码的一个因素),获得更快的速度。然而,由于map性质清晰明确,无需手动管理线程,以干净、可靠、易于调试的方式混合匹配两者(译注:这里的“两者”是指什么?CPU密集型工作和IO密集型工作?),也是相当容易的。

    就是这样了。(几乎)一行式并行解决方案。

    转自:http://blog.xiayf.cn/2015/09/11/parallelism-in-one-line/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

  • 相关阅读:
    104. 二叉树的最大深度
    1120. 子树的最大平均值
    1121. 将数组分成几个递增序列
    1118. 一月有多少天
    1110. 删点成林
    102. 二叉树的层次遍历
    145. 二叉树的后序遍历
    94. 二叉树的中序遍历
    144. 二叉树的前序遍历
    剑指offer-0x04
  • 原文地址:https://www.cnblogs.com/wangxusummer/p/4835929.html
Copyright © 2011-2022 走看看