Python多线程辣鸡?那要怎样并行运算呢?

前言

Python在并行运算方面因为GIL(Global Interpreter Lock,全局解释器锁)而饱受诟病,认为Python的多线程其实是伪的,很鸡肋,这里就大致讲解下吧,
在Python的原始解释器CPython中存在着GIL,因此在解释执行Python代码时,会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数达到一定数目时才会释放GIL
所以有GIL效果就是:一个进程内同一时间只能允许一个线程进行运算 (这尼玛不就是单线程吗?)
至于为什么要有GIL?只能说这是个历史遗留问题了,人家发明Python的时候压根就没想到现在居然有多核CPU,甚至多CPU的电脑啊~
再至于为什么GIL没有被优化掉,总是有人家的考虑的,反正Python3也继续了GIL的优良传统,爱用不用,有兴趣的自行搜索GIL吧
我这里尽量用事实说话,直接黑盒测试下常见的几种并行运算方式

正文

测试环境:
电脑:


我的电脑

Python: 2.7.10

我都是使用 multiprocessing 模块进行对比,对比三种常见的使用情况,我分别取名(非官方):ThreadPool,DummyPool,ProcessPool

引入方式如下:

from multiprocessing.pool import ThreadPool
from multiprocessing.dummy import Pool as DummyPool
from multiprocessing import Pool as ProcessPool

说明: ThreadPool,DummyPool 都是线程池,ProcessPool 是进程池

测试代码如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# by vellhe 2017/7/1

from multiprocessing.pool import ThreadPool
from multiprocessing.dummy import Pool as DummyPool
from multiprocessing import Pool as ProcessPool

import time

max_range = 10000000


def run(i):
    i = i * i
    # return i # return和不return对进程池运行速度会有比较大影响,不return效率更高


def thread_pool(num):
    p = ThreadPool(num)
    start_time = time.time()
    ret = p.map(run, range(max_range))
    p.close()
    p.join()
    print("thread_pool  %d, costTime: %fs ret.size: %d" % (num, (time.time() - start_time), len(ret)))


def dummy_pool(num):
    p = DummyPool(num)
    start_time = time.time()
    ret = p.map(run, range(max_range))
    p.close()
    p.join()
    print("dummy_pool   %d, costTime: %fs ret.size: %d" % (num, (time.time() - start_time), len(ret)))


def process_pool(num):
    p = ProcessPool(num)
    start_time = time.time()
    ret = p.map(run, range(max_range))
    p.close()
    p.join()
    print("process_pool %d, costTime: %fs ret.size: %d" % (num, (time.time() - start_time), len(ret)))


if __name__ == "__main__":
    for i in range(1, 9):
        thread_pool(i)
        dummy_pool(i)
        process_pool(i)
        print("=====")

测试说明:

通过并行计算max_range次对于i的二次方,没有任何IO操作,纯运算
这里特别说明,由于偶然发现run方法return和不returnProcessPool会有很大影响,所以前后分别跑了两次

测试结果:

  • 没有return:


    没有return

    从上图很容易得出以下结论:

    • thread_pool和dummy_pool运行速度几乎没有什么区别,因为都是线程池,而且从实现代码分析,其实dummy_pool就是thread_pool,只是套了一层壳而已

      DummyPool实现

    • 单线程运行速度比多线程要快,这就是因为python的GIL机制了,一个进程内同一时间只能允许一个线程进行运算,多线程只会让时间白白在线程间切换上了。
      这时有人会说,那python的多线程不就废了,要它何用?
      其实不然,这里只是做了纯运算的实验,没有任何IO,如果是高IO的话情况就不一样了,因为在等待IO完成时会去处理另外的线程,而IO往往耗时较高,所以在一些高IO情况下(如批量处理文件、网络请求、爬虫等)还是可以合理使用python的多线程的

    • 单进程运行比单线程慢,这个也能理解,毕竟开一个线程比开一个进程要简单得多,没有资源分配等乱七八糟的东西

    • 多进程比单进程运行快,这要是不快就奇怪了,毕竟多进程是分散在不同cpu核上跑的,这里和多线程比优势就很明显了,所以一些科学运算想要提升速度就会用多进程策略了

    • 当多进程数够多情况下会超越多线程的速度,原因很简单,多线程并不会因为线程数增多而变快,而多进程却可以,所以超越是必然的

  • 有return:


    有return

    拿这张图对比上面没有return的那张就会发现一些有意思的事情:

    • 有return后单线程和多线程速度几乎一致了,但多进程还是比单进程要快很多,由于我电脑是4核的,所以还能大致看出,4个进程以后速度并没有提升了
    • 有return后总体运算速度都慢了,特别是进程的速度,慢了一倍,这里原因是进程间通信耗时较大,需要把结果return到主进程中,所以做大量运算时尽量避免进程间通信

测试存在IO操作的情况

上面都是纯运算,没有IO,接下来就看看存在IO操作会是什么样吧,代码如下,在循环计算前增加request请求

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# by vellhe 2017/7/1
import time
from multiprocessing.pool import ThreadPool
from multiprocessing import Pool as ProcessPool
import requests

max_range = 50


def run(i):
    requests.get("http://www.qq.com")
    for x in range(10000):
        i += i * x
    return i


def thread_pool(num):
    p = ThreadPool(num)
    start_time = time.time()
    ret = p.map(run, range(max_range))
    p.close()
    p.join()
    print("thread_pool  %d, costTime: %fs ret.size: %d" % (num, (time.time() - start_time), len(ret)))


def process_pool(num):
    p = ProcessPool(num)
    start_time = time.time()
    ret = p.map(run, range(max_range))
    p.close()
    p.join()
    print("process_pool %d, costTime: %fs ret.size: %d" % (num, (time.time() - start_time), len(ret)))


if __name__ == "__main__":
    for i in range(1, 9):
        thread_pool(i)
        process_pool(i)
        print("=====")

测试结果:


IO操作结果

由上图可知:

  • 存在IO操作的话,python的多线程才会有用武之地,有效提升了速度
  • 存在IO情况下,多进程效率还是会比多线程高很多
  • 进程和线程数都是在4个后速度没有再提升了,因为我电脑是4核的

继续追加实验,看看多进程和多线程下CPU使用情况

分别做了好几次实验,惊奇的发现个很神奇的事情,不管开多少个进程或者线程,每次cpu核占用情况都是大致如下:


cpu使用情况

结果并没有出现我想像中的,单进程是一个核占用暴涨,其它核都是休息状,我也解释不了为什么了,难道是multiprocessing有优化?还是系统层做了优化?所以再做了一个实验,没有用任何进程池,直接for循环计算:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# by vellhe 2017/7/1
import time

max_range = 100000000


def run(i):
    i = i * i
    return i


if __name__ == "__main__":
    start_time = time.time()
    for i in range(max_range):
        run(i)
    print("costTime: %fs" % (time.time() - start_time))

结果居然还是各个核的占用情况几乎是均匀的,所以几乎可以断定,这是系统层的优化了,所以先告一段落吧,以后再继续深究

后语

来个大致总结吧,针对python而言:

  • 纯运算情况下单线程比多线程更快
  • 多线程在IO操作较多情况下才能很好的发挥作用,但效率还是低于多进程
  • 单进程运行比单线程慢,但当多进程数够多情况下会超越单线程的速度
  • 多进程比单进程运行快
  • 对于多进程而言,有return会比没有return慢很多很多,对于多线程却只会慢一点点
  • 【疑惑】不管开多少个进程或者线程,各个核占用情况几乎是均匀的,猜测是系统底层有优化

ps:关于我的疑惑,知道明确结论的大侠们请给我留言,多谢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容