教你用一行Python代码实现并行任务(附代码)


Posted in Python onFebruary 02, 2018

Python在程序并行化方面多少有些声名狼藉。撇开技术上的问题,例如线程的实现和GIL,我觉得错误的教学指导才是主要问题。常见的经典Python多线程、多进程教程多显得偏"重"。而且往往隔靴搔痒,没有深入探讨日常工作中最有用的内容。

传统的例子

简单搜索下"Python多线程教程",不难发现几乎所有的教程都给出涉及类和队列的例子:

#Example.py
'''
Standard Producer/Consumer Threading Pattern
'''
import time 
import threading 
import Queue 
class Consumer(threading.Thread): 
  def __init__(self, queue): 
    threading.Thread.__init__(self)
    self._queue = queue 
  def run(self):
    while True: 
      # queue.get() blocks the current thread until 
      # an item is retrieved. 
      msg = self._queue.get() 
      # Checks if the current message is 
      # the "Poison Pill"
      if isinstance(msg, str) and msg == 'quit':
        # if so, exists the loop
        break
      # "Processes" (or in our case, prints) the queue item  
      print "I'm a thread, and I received %s!!" % msg
    # Always be friendly! 
    print 'Bye byes!'
def Producer():
  # Queue is used to share items between
  # the threads.
  queue = Queue.Queue()
  # Create an instance of the worker
  worker = Consumer(queue)
  # start calls the internal run() method to 
  # kick off the thread
  worker.start() 
  # variable to keep track of when we started
  start_time = time.time() 
  # While under 5 seconds.. 
  while time.time() - start_time < 5: 
    # "Produce" a piece of work and stick it in 
    # the queue for the Consumer to process
    queue.put('something at %s' % time.time())
    # Sleep a bit just to avoid an absurd number of messages
    time.sleep(1)
  # This the "poison pill" method of killing a thread. 
  queue.put('quit')
  # wait for the thread to close down
  worker.join()
if __name__ == '__main__':
  Producer()

哈,看起来有些像 Java 不是吗?

我并不是说使用生产者/消费者模型处理多线程/多进程任务是错误的(事实上,这一模型自有其用武之地)。只是,处理日常脚本任务时我们可以使用更有效率的模型。

问题在于…

首先,你需要一个样板类;
其次,你需要一个队列来传递对象;
而且,你还需要在通道两端都构建相应的方法来协助其工作(如果需想要进行双向通信或是保存结果还需要再引入一个队列)。

worker越多,问题越多

按照这一思路,你现在需要一个worker线程的线程池。下面是一篇IBM经典教程中的例子——在进行网页检索时通过多线程进行加速。

#Example2.py
'''
A more realistic thread pool example 
'''
import time 
import threading 
import Queue 
import urllib2 
class Consumer(threading.Thread): 
  def __init__(self, queue): 
    threading.Thread.__init__(self)
    self._queue = queue 
  def run(self):
    while True: 
      content = self._queue.get() 
      if isinstance(content, str) and content == 'quit':
        break
      response = urllib2.urlopen(content)
    print 'Bye byes!'
def Producer():
  urls = [
    'http://www.python.org', 'http://www.yahoo.com'
    'http://www.scala.org', 'http://www.google.com'
    # etc.. 
  ]
  queue = Queue.Queue()
  worker_threads = build_worker_pool(queue, 4)
  start_time = time.time()
  # Add the urls to process
  for url in urls: 
    queue.put(url) 
  # Add the poison pillv
  for worker in worker_threads:
    queue.put('quit')
  for worker in worker_threads:
    worker.join()
  print 'Done! Time taken: {}'.format(time.time() - start_time)
def build_worker_pool(queue, size):
  workers = []
  for _ in range(size):
    worker = Consumer(queue)
    worker.start() 
    workers.append(worker)
  return workers
if __name__ == '__main__':
  Producer()

这段代码能正确的运行,但仔细看看我们需要做些什么:构造不同的方法、追踪一系列的线程,还有为了解决恼人的死锁问题,我们需要进行一系列的join操作。这还只是开始……

至此我们回顾了经典的多线程教程,多少有些空洞不是吗?样板化而且易出错,这样事倍功半的风格显然不那么适合日常使用,好在我们还有更好的方法。

何不试试 map

map这一小巧精致的函数是简捷实现Python程序并行化的关键。map源于Lisp这类函数式编程语言。它可以通过一个序列实现两个函数之间的映射。

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

上面的这两行代码将 urls 这一序列中的每个元素作为参数传递到 urlopen 方法中,并将所有结果保存到 results 这一列表中。其结果大致相当于:

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

map 函数一手包办了序列操作、参数传递和结果保存等一系列的操作。

为什么这很重要呢?这是因为借助正确的库,map可以轻松实现并行化操作。

在Python中有个两个库包含了map函数: multiprocessing和它鲜为人知的子库 multiprocessing.dummy.

这里多扯两句:multiprocessing.dummy? mltiprocessing库的线程版克隆?这是虾米?即便在multiprocessing库的官方文档里关于这一子库也只有一句相关描述。而这句描述译成人话基本就是说:"嘛,有这么个东西,你知道就成."相信我,这个库被严重低估了!

dummy是multiprocessing模块的完整克隆,唯一的不同在于multiprocessing作用于进程,而dummy模块作用于线程(因此也包括了Python所有常见的多线程限制)。

所以替换使用这两个库异常容易。你可以针对IO密集型任务和CPU密集型任务来选择不同的库。

动手尝试

使用下面的两行代码来引用包含并行化map函数的库:

from multiprocessing import Pool
from multiprocessing.dummy import Pool as ThreadPool

实例化 Pool 对象:

pool = ThreadPool()

这条简单的语句替代了example2.py中buildworkerpool函数7行代码的工作。它生成了一系列的worker线程并完成初始化工作、将它们储存在变量中以方便访问。

Pool对象有一些参数,这里我所需要关注的只是它的第一个参数:processes. 这一参数用于设定线程池中的线程数。其默认值为当前机器CPU的核数。

一般来说,执行CPU密集型任务时,调用越多的核速度就越快。但是当处理网络密集型任务时,事情有有些难以预计了,通过实验来确定线程池的大小才是明智的。

pool = ThreadPool(4) # Sets the pool size to 4

线程数过多时,切换线程所消耗的时间甚至会超过实际工作时间。对于不同的工作,通过尝试来找到线程池大小的最优值是个不错的主意。

创建好Pool对象后,并行化的程序便呼之欲出了。我们来看看改写后的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/'
  # etc.. 
  ]
# Make the Pool of workers
pool = ThreadPool(4) 
# Open the urls in their own threads
# and return the results
results = pool.map(urllib2.urlopen, urls)
#close the pool and wait for the work to finish 
pool.close() 
pool.join()

实际起作用的代码只有4行,其中只有一行是关键的。map函数轻而易举的取代了前文中超过40行的例子。为了更有趣一些,我统计了不同方法、不同线程池大小的耗时情况。

# results = [] 
# for url in urls:
#  result = urllib2.urlopen(url)
#  results.append(result)
# # ------- VERSUS ------- # 
# # ------- 4 Pool ------- # 
# pool = ThreadPool(4) 
# results = pool.map(urllib2.urlopen, urls)
# # ------- 8 Pool ------- # 
# pool = ThreadPool(8) 
# results = pool.map(urllib2.urlopen, urls)
# # ------- 13 Pool ------- # 
# pool = ThreadPool(13) 
# results = pool.map(urllib2.urlopen, urls)

结果:

#        Single thread:  14.4 Seconds
#               4 Pool:   3.1 Seconds
#               8 Pool:   1.4 Seconds
#              13 Pool:   1.3 Seconds

很棒的结果不是吗?这一结果也说明了为什么要通过实验来确定线程池的大小。在我的机器上当线程池大小大于9带来的收益就十分有限了。

另一个真实的例子

生成上千张图片的缩略图

这是一个CPU密集型的任务,并且十分适合进行并行化。

基础单进程版本

import os 
import PIL 
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循环:

import os 
import PIL 
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(creat_thumbnail, images)
  pool.close()
  pool.join()

5.6 秒!

虽然只改动了几行代码,我们却明显提高了程序的执行速度。在生产环境中,我们可以为CPU密集型任务和IO密集型任务分别选择多进程和多线程库来进一步提高执行速度——这也是解决死锁问题的良方。此外,由于map函数并不支持手动线程管理,反而使得相关的debug工作也变得异常简单。

到这里,我们就实现了(基本)通过一行Python实现并行化。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
对比Python中__getattr__和 __getattribute__获取属性的用法
Jun 21 Python
Python中音频处理库pydub的使用教程
Jun 07 Python
python实现rsa加密实例详解
Jul 19 Python
python+pandas分析nginx日志的实例
Apr 28 Python
Python画柱状统计图操作示例【基于matplotlib库】
Jul 04 Python
python实现三次样条插值
Dec 17 Python
Django使用Jinja2模板引擎的示例代码
Aug 09 Python
Python Gitlab Api 使用方法
Aug 28 Python
python 哈希表实现简单python字典代码实例
Sep 27 Python
python生成器推导式用法简单示例
Oct 08 Python
python如何查看网页代码
Jun 07 Python
python实现图片批量压缩
Apr 24 Python
Python实现Pig Latin小游戏实例代码
Feb 02 #Python
python在线编译器的简单原理及简单实现代码
Feb 02 #Python
使用Python进行AES加密和解密的示例代码
Feb 02 #Python
为什么选择python编程语言入门黑客攻防 给你几个理由!
Feb 02 #Python
Python无损音乐搜索引擎实现代码
Feb 02 #Python
Python面向对象class类属性及子类用法分析
Feb 02 #Python
Python网络编程之TCP与UDP协议套接字用法示例
Feb 02 #Python
You might like
php inc文件使用的风险和注意事项
2013/11/12 PHP
标准PHP的AES加密算法类
2015/03/12 PHP
总结的一些PHP开发中的tips(必看篇)
2017/03/24 PHP
php 自定义函数实现将数据 以excel 表格形式导出示例
2019/11/13 PHP
自己的js工具 Cookie 封装
2009/08/21 Javascript
几个有趣的Javascript Hack
2010/07/24 Javascript
js判断背景图片是否加载成功使用img的width实现
2013/05/29 Javascript
javascript关于继承的用法汇总
2014/12/20 Javascript
解决WordPress使用CDN后博文无法评论的错误
2015/12/15 Javascript
javascript原型继承工作原理和实例详解
2016/04/07 Javascript
Bootstrap弹出框modal上层的输入框不能获得焦点问题的解决方法
2016/12/13 Javascript
AngularJS中使用three.js的实例详解
2017/07/21 Javascript
微信小程序中悬浮窗功能的实现代码
2019/08/02 Javascript
原生JS与JQ获取元素的区别详解
2020/02/13 Javascript
完美解决python遍历删除字典里值为空的元素报错问题
2016/09/11 Python
mac下pycharm设置python版本的图文教程
2018/06/13 Python
详解Django的model查询操作与查询性能优化
2018/10/16 Python
python之Flask实现简单登录功能的示例代码
2018/12/24 Python
Python操作配置文件ini的三种方法讲解
2019/02/22 Python
如何通过python实现人脸识别验证
2020/01/17 Python
Python使用uuid库生成唯一标识ID
2020/02/12 Python
Python3 获取文件属性的方式(时间、大小等)
2020/03/12 Python
Django {{ MEDIA_URL }}无法显示图片的解决方式
2020/04/07 Python
python安装读取grib库总结(推荐)
2020/06/24 Python
ubuntu16.04升级Python3.5到Python3.7的方法步骤
2020/08/20 Python
世界著名的顶级牛排:Omaha Steak(奥马哈牛排)
2016/09/20 全球购物
捷克时尚网上商店:OTTO
2018/03/15 全球购物
西班牙灯具网上商店:Lampara.es
2018/06/05 全球购物
Janie and Jack美国官网:GAP旗下的高档童装品牌
2019/09/09 全球购物
大学生毕业自我评价范文分享
2013/11/11 职场文书
护士求职推荐信范文
2013/11/23 职场文书
2014年职称评定工作总结
2014/11/26 职场文书
小学六一儿童节活动总结
2015/05/05 职场文书
2015年政治教研组工作总结
2015/07/22 职场文书
浅谈css实现背景颜色半透明的两种方法
2021/12/06 HTML / CSS
pd.DataFrame中的几种索引变换的实现
2022/06/16 Python