教你用一行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 Web框架Flask中使用百度云存储BCS实例
Feb 08 Python
python连接远程ftp服务器并列出目录下文件的方法
Apr 01 Python
Python的Django框架下管理站点的基本方法
Jul 17 Python
在Django的模型中添加自定义方法的示例
Jul 21 Python
Python基础教程之tcp socket编程详解及简单实例
Feb 23 Python
Python中Django 后台自定义表单控件
Mar 28 Python
python实现推箱子游戏
Mar 25 Python
libreoffice python 操作word及excel文档的方法
Jul 04 Python
Python @property使用方法解析
Sep 17 Python
Python猜数字算法题详解
Mar 01 Python
pycharm 多行批量缩进和反向缩进快捷键介绍
Jan 15 Python
python对文档中元素删除,替换操作
Apr 02 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
使用ltrace工具跟踪PHP库函数调用的方法
2016/04/25 PHP
Android AsyncTack 异步任务实例详解
2016/11/02 PHP
PHP获取IP地址所在地信息的实例(使用纯真IP数据库qqwry.dat)
2016/11/15 PHP
PHP中抽象类,接口功能、定义方法示例
2019/02/26 PHP
Yii框架数据库查询、增加、删除操作示例
2019/10/14 PHP
利用js获取服务器时间的两个简单方法
2010/01/08 Javascript
jquery+ashx无刷新GridView数据显示插件(实现分页、排序、过滤功能)
2010/04/25 Javascript
网站页面自动跳转实现方法PHP、JSP(下)
2010/08/01 Javascript
情人节专属 纯js脚本1k大小的3D玫瑰效果
2012/02/11 Javascript
为jquery的ajaxfileupload增加附加参数的方法
2014/03/04 Javascript
一个JavaScript操作元素定位元素的实例
2014/10/29 Javascript
javascript事件委托的方式绑定详解
2015/06/10 Javascript
JQuery实现Ajax加载图片的方法
2015/12/24 Javascript
JS加载iFrame出现空白问题的解决办法
2016/05/13 Javascript
jquery easyui validatebox remote的使用详解
2016/11/09 Javascript
浅谈js中几种实用的跨域方法原理详解
2016/12/02 Javascript
JavaScript实现动态增删表格的方法
2017/03/09 Javascript
ES6中Iterator与for..of..遍历用法分析
2017/03/31 Javascript
详解基于Node.js的微信JS-SDK后端接口实现代码
2017/07/15 Javascript
JavaScript实现与使用发布/订阅模式详解
2019/01/19 Javascript
简单总结Python中序列与字典的相同和不同之处
2016/01/19 Python
python微信公众号之关键词自动回复
2018/06/15 Python
解决python3 urllib 链接中有中文的问题
2018/07/16 Python
Django使用redis缓存服务器的实现代码示例
2019/04/28 Python
Pandas时间序列基础详解(转换,索引,切片)
2020/02/26 Python
解析pip安装第三方库但PyCharm中却无法识别的问题及PyCharm安装第三方库的方法教程
2020/03/10 Python
深入了解Python 变量作用域
2020/07/24 Python
一篇文章搞懂python的转义字符及用法
2020/09/03 Python
纯CSS3+DIV实现小三角形边框效果的示例代码
2020/08/03 HTML / CSS
英国领先的男装设计师服装购物网站:Mainline Menswear
2018/02/04 全球购物
Yahoo-PHP面试题2
2014/12/06 面试题
教师的实习自我鉴定
2013/12/17 职场文书
大课间活动实施方案
2014/03/06 职场文书
初中教师德育工作总结2015
2015/05/12 职场文书
2019感恩宣传标语!
2019/07/05 职场文书
介绍一下28个JS常用数组方法
2022/05/06 Javascript