Python之threading多线程 存在的意义


 

在群里经常听到这样的争执,有人是虚心请教问题,有人就大放厥词因为这个说python辣鸡。而争论的核心无非就是,python的多线程在同一时刻只会有一条线程跑在CPU里面,其他线程都在睡觉。这是真的吗?

是真的。这个就是因为传说中的GIL(全局解释锁)的存在。不明白这个词的可以去百度一下,我解释不好(大家都是程序猿你懂的,我写错一个词就要挨喷了,就算我没写错对方理解错了,我也一样要挨喷)。有了这样一个看似bug的存在,就导致了上面说的情况:同一时刻内,python的线程只有一条在CPU里面运行

所以python的多线程就没用咯?当然不是。这要看程序是什么样的。如果是一个计算为主的程序(专业一点称为CPU密集型程序),这一点确实是比较吃亏的,每个线程运行一遍,就相当于单线程再跑,甚至比单线程还要慢——CPU切换线程的上下文也是要有开销的。但是,如果是一个磁盘或网络为主的程序(IO密集型)就不同了。一个线程处在IO等待的时候,另一个线程还可以在CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的,python的多线程就在这时候发挥作用了。比方说多线程网络传输,多线程往不同的目录写文件,等等。

话说回来,CPU密集型的程序用python来做,本身就不合适。跟C,Go,Java的速度比,实在性能差到没法说。你当然可以写个C扩展来实现真正的多线程用python来调用,那样速度是快。我们之所以用python来做,只是因为开发效率超高,可以快速实现。

最后补充几点:

  1. python中要想利用好CPU,还是用多进程来做吧。或者,可以使用协程。multiprocessing和gevent在召唤你。
  2. GIL不是bug,Guido也不是水平有限才留下这么个东西。龟叔曾经说过,尝试不用GIL而用其他的方式来做线程安全,结果python语言整体效率又下降了一倍,权衡利弊,GIL是最好的选择——不是去不掉,而是故意留着的。
  3. 想让python计算速度快起来,又不想写C?用pypy吧,这才是真正的大杀器。

 

 

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

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

 

 

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

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

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

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

动手尝试

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

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

 

实例化 Pool 对象:

pool = ThreadPool()
 

 

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

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

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

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

 

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

创建好 Pool 对象后,并行化的程序便呼之欲出了。我们来看看改写后的 example2.py

 

  1.  
    import urllib2
  2.  
    from multiprocessing.dummy import Pool as ThreadPool
  3.  
     
  4.  
    urls = [
  5.  
    'http://www.python.org',
  6.  
    'http://www.python.org/about/',
  7.  
    'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html',
  8.  
    'http://www.python.org/doc/',
  9.  
    'http://www.python.org/download/',
  10.  
    'http://www.python.org/getit/',
  11.  
    'http://www.python.org/community/',
  12.  
    'https://wiki.python.org/moin/',
  13.  
    'http://planet.python.org/',
  14.  
    'https://wiki.python.org/moin/LocalUserGroups',
  15.  
    'http://www.python.org/psf/',
  16.  
    'http://docs.python.org/devguide/',
  17.  
    'http://www.python.org/community/awards/'
  18.  
    # etc..
  19.  
    ]
  20.  
     
  21.  
    # Make the Pool of workers
  22.  
    pool = ThreadPool( 4)
  23.  
    # Open the urls in their own threads
  24.  
    # and return the results
  25.  
    results = pool.map(urllib2.urlopen, urls)
  26.  
    #close the pool and wait for the work to finish
  27.  
    pool.close()
  28.  
    pool.join()

 

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

  1.  
    # results = []
  2.  
    # for url in urls:
  3.  
    # result = urllib2.urlopen(url)
  4.  
    # results.append(result)
  5.  
     
  6.  
    # # ------- VERSUS ------- #
  7.  
     
  8.  
    # # ------- 4 Pool ------- #
  9.  
    # pool = ThreadPool(4)
  10.  
    # results = pool.map(urllib2.urlopen, urls)
  11.  
     
  12.  
    # # ------- 8 Pool ------- #
  13.  
     
  14.  
    # pool = ThreadPool(8)
  15.  
    # results = pool.map(urllib2.urlopen, urls)
  16.  
     
  17.  
    # # ------- 13 Pool ------- #
  18.  
     
  19.  
    # pool = ThreadPool(13)
  20.  
    # results = pool.map(urllib2.urlopen, urls)
  21.  
     
  22.  
    结果:
  23.  
     
  24.  
    # Single thread: 14.4 Seconds
  25.  
    # 4 Pool: 3.1 Seconds
  26.  
    # 8 Pool: 1.4 Seconds
  27.  
    # 13 Pool: 1.3 Seconds

 

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

另一个真实的例子

生成上千张图片的缩略图

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

基础单进程版本

  1.  
    import os
  2.  
    import PIL
  3.  
     
  4.  
    from multiprocessing import Pool
  5.  
    from PIL import Image
  6.  
     
  7.  
    SIZE = ( 75,75)
  8.  
    SAVE_DIRECTORY = 'thumbs'
  9.  
     
  10.  
    def get_image_paths(folder):
  11.  
    return (os.path.join(folder, f)
  12.  
    for f in os.listdir(folder)
  13.  
    if 'jpeg' in f)
  14.  
     
  15.  
    def create_thumbnail(filename):
  16.  
    im = Image.open(filename)
  17.  
    im.thumbnail(SIZE, Image.ANTIALIAS)
  18.  
    base, fname = os.path.split(filename)
  19.  
    save_path = os.path.join(base, SAVE_DIRECTORY, fname)
  20.  
    im.save(save_path)
  21.  
     
  22.  
    if __name__ == '__main__':
  23.  
    folder = os.path.abspath(
  24.  
    '11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840')
  25.  
    os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
  26.  
     
  27.  
    images = get_image_paths(folder)
  28.  
     
  29.  
    for image in images:
  30.  
    create_thumbnail(Image)



 

上边这段代码的主要工作就是将遍历传入的文件夹中的图片文件,一一生成缩略图,并将这些缩略图保存到特定文件夹中。

这我的机器上,用这一程序处理 6000 张图片需要花费 27.9 秒。

如果我们使用 map 函数来代替 for 循环:

  1.  
    import os
  2.  
    import PIL
  3.  
     
  4.  
    from multiprocessing import Pool
  5.  
    from PIL import Image
  6.  
     
  7.  
    SIZE = ( 75,75)
  8.  
    SAVE_DIRECTORY = 'thumbs'
  9.  
     
  10.  
    def get_image_paths(folder):
  11.  
    return (os.path.join(folder, f)
  12.  
    for f in os.listdir(folder)
  13.  
    if 'jpeg' in f)
  14.  
     
  15.  
    def create_thumbnail(filename):
  16.  
    im = Image.open(filename)
  17.  
    im.thumbnail(SIZE, Image.ANTIALIAS)
  18.  
    base, fname = os.path.split(filename)
  19.  
    save_path = os.path.join(base, SAVE_DIRECTORY, fname)
  20.  
    im.save(save_path)
  21.  
     
  22.  
    if __name__ == '__main__':
  23.  
    folder = os.path.abspath(
  24.  
    '11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840')
  25.  
    os.mkdir(os.path.join(folder, SAVE_DIRECTORY))
  26.  
     
  27.  
    images = get_image_paths(folder)
  28.  
     
  29.  
    pool = Pool()
  30.  
    pool.map(creat_thumbnail, images)
  31.  
    pool.close()
  32.  
    pool.join()



5.6 秒!

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

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

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM