教你用一行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编写检测数据库SA用户的方法
Jul 11 Python
剖析Python的Tornado框架中session支持的实现代码
Aug 21 Python
Python简单实现自动删除目录下空文件夹的方法
Aug 29 Python
基于Python代码编辑器的选用(详解)
Sep 13 Python
PyQt5 QSerialPort子线程操作的实现
Apr 21 Python
使用python3实现操作串口详解
Jan 01 Python
Python代码块及缓存机制原理详解
Dec 13 Python
python 爬取马蜂窝景点翻页文字评论的实现
Jan 20 Python
Python流程控制语句的深入讲解
Jun 15 Python
Python CategoricalDtype自定义排序实现原理解析
Sep 11 Python
利用python 下载bilibili视频
Nov 13 Python
python字符串的多行输出的实例详解
Jun 08 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
实用函数4
2007/11/08 PHP
PHP抓取、分析国内视频网站的视频信息工具类
2014/04/02 PHP
php获取当前url地址的方法小结
2017/01/10 PHP
php实现购物车功能(以大苹果购物网为例)
2017/03/09 PHP
javascript基础之查找元素的详细介绍(访问节点)
2013/07/05 Javascript
javaScript 计算两个日期的天数相差(示例代码)
2013/12/27 Javascript
JavaScript不刷新实现浏览器的前进后退功能
2014/11/05 Javascript
javascript实现的多个层切换效果通用函数实例
2015/07/06 Javascript
跨域请求的完美解决方法(JSONP, CORS)
2016/06/12 Javascript
jQuery表单验证之密码确认
2017/05/22 jQuery
JavaScript之Date_动力节点Java学院整理
2017/06/28 Javascript
js 提取某()特殊字符串长度的实例
2017/12/06 Javascript
使用webpack-dev-server处理跨域请求的方法
2018/04/18 Javascript
详解使用Next.js构建服务端渲染应用
2018/07/10 Javascript
Echart折线图手柄触发事件示例详解
2018/12/16 Javascript
js中Generator函数的深入讲解
2019/04/07 Javascript
JavaScript使用百度ECharts插件绘制饼图操作示例
2019/11/26 Javascript
Python深入学习之特殊方法与多范式
2014/08/31 Python
Python抓取京东图书评论数据
2014/08/31 Python
win与linux系统中python requests 安装
2016/12/04 Python
Python安装Numpy和matplotlib的方法(推荐)
2017/11/02 Python
python基础练习之几个简单的游戏
2017/11/10 Python
Python3使用turtle绘制超立方体图形示例
2018/06/19 Python
Linux下远程连接Jupyter+pyspark部署教程
2019/06/21 Python
Python如何基于selenium实现自动登录博客园
2019/12/16 Python
pycharm激活方法到2099年(激活流程)
2020/09/22 Python
Python非单向递归函数如何返回全部结果
2020/12/18 Python
python 中 .py文件 转 .pyd文件的操作
2021/03/04 Python
塑料制成的可水洗的编织平底鞋和鞋子:Rothy’s
2018/09/16 全球购物
skyn ICELAND官网:冰岛成分天然护肤品
2020/08/24 全球购物
初一英语教学反思
2014/01/11 职场文书
体育比赛口号
2014/06/09 职场文书
销售目标责任书
2014/07/23 职场文书
企业党的群众路线教育实践活动领导班子对照检查材料
2014/09/25 职场文书
自主招生自荐信格式范文
2015/03/25 职场文书
Android超详细讲解组件ScrollView的使用
2022/03/31 Java/Android