原创

谈谈Python多线程

本文环境: Python 2.7.10 (CPython)。

TOC

  • 因为GIL的存在,Python多线程是否鸡肋?
  • 既然已有GIL,是否Python编程不需要关注线程安全的问题?不需要使用锁?
  • 为什么Python进阶材料很少有讲解多线程?

一、GIL简介

首先我们看下Global Interpreter Lock(GIL)的官方介绍:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

简而言之,因为CPython的内存管理不是线程安全的,所以需要加一个全局解释锁来保障Python内部对象是线程安全的。

GIL的存在导致Python多线程是不完整的多线程,Python社区内部对是否保留GIL一致激烈讨论,这里我们就不在累述。

二、Python多线程是否鸡肋

正如上节所说,Python的多线程是不完整的多线程。不过抛开具体应用场景谈“Python多线程是否鸡肋”就是耍流氓了!

1. 为什么需要多线程呢?

为什么需要多线程呢?总结一下,多线程多应用在如下场景:

  • 需要运行后台任务但不希望停止主线程的执行
    • 定期打印日志
    • 图形界面下,主循环需要等待事件
  • 分散任务负载
    • 高负载任务一般分计算密集型、IO密集型两类。

2. 计算密集型 vs. IO密集型

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。

IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少。

计算密集型验证例子

Python作为一门脚本语言,本身执行效率极低,完全不适合计算密集型任务的开发。再加上GIL的存在,需要花费大量时间用在线程间的切换,其多线程性能甚至低于单线程。以下是一个验证例子:

顺序执行的单线程(single_thread.py)

#! /usr/bin/python
 
from threading import Thread
import time
 
def my_counter():
    i = 0
    for _ in range(100000000):
        i = i + 1
    return True
 
def main():
    thread_array = {}
    start_time = time.time()
    for tid in range(2):
        t = Thread(target=my_counter)
        t.start()
        t.join()
    end_time = time.time()
    print("Total time: {}".format(end_time - start_time))
 
if __name__ == '__main__':
    main()

同时执行的两个并发线程(multi_thread.py)

#! /usr/bin/python
 
from threading import Thread
import time
 
def my_counter():
    i = 0
    for _ in range(100000000):
        i = i + 1
    return True
 
def main():
    thread_array = {}
    start_time = time.time()
    for tid in range(2):
        t = Thread(target=my_counter)
        t.start()
        thread_array[tid] = t
    for i in range(2):
        thread_array[i].join()
    end_time = time.time()
    print("Total time: {}".format(end_time - start_time))
 
if __name__ == '__main__':
    main()
image.png

多线程执行更慢了!

经过大量测试,Python多线程下一般最多只能占用1.5~2核,完全无法充分利用CPU资源。

3.小结

在低计算场景(普通后台任务、IO密集型任务)下,Python多线程还是有一点用武之地。但是计算密集型任务的话,Python多线程是真鸡肋,甚至会严重拖后腿。

三、锁与线程安全

既然有GIL这个语言级的锁,那我们是不是可以不关注锁与线程安全,直接起飞了?

且看下面这个例子

#! /usr/bin/python
 
import threading

i = 0

def test():
    global i
    for x in range(100000):
        i += 1

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

assert i == 100000, i

显然失败了。因为高级语言的一条语句执行时都是分为若干条执行码,即使一个简单的计算:i += 1,也是分为4个执行码。

  • load i
  • load 1
  • add
  • store it back to i

Python解释器默认每100个操作码切换一个活动线程(通过从一个线程释放GIL以便另一个线程可以使用)。当100个操作码切换时,就会出现争抢,从而出现线程不安全的情况。此时就需要我们加一个简单的锁。

#!/usr/bin/python
import threading
i = 0
i_lock = threading.Lock()

def test():
    global i
    i_lock.acquire()
    try:
        for x in range(100000):
            i += 1
    finally:
        i_lock.release()

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

assert i == 100000, i

四、总结

相比Java那种天生面向多线程的语言不同,Python本身多线程就是不太完善的多线程。GIL的存在导致多线程CPU利用效率甚至低于单线程,却仍然要面对锁与线程安全的问题。同时Python语言又不像Java自带多种线程安全的数据类型,增加了多线程编程的复杂度,所以很少有资料大篇幅讲解Python多线程。

正如《Python高手之路》所言: (Python)处理好多线程是很难的,其复杂程度意味着与其他方式(异步事件\多进程)相比它是bug的更大来源,而且考虑到通常能够获取的好处很少,所以最好不要在多线程上浪费太多精力。

参考资料:

python中的GIL详解

is-the-operator-thread-safe-in-python

《Python高手之路》(《The Hacker's Guide to Python》)

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 爬虫学习开篇

    在这个大数据时代,尤其是人工浪潮兴起的时代,不论是工程领域还是研究领域,数据已经成为必不可少的一部分,而数据的获取很大程度上依赖于爬虫的爬取,所以爬虫也逐渐变得...

    stormwen
  • 《快学BigData》--Hadoop总结(G)(40)

    Hadoop总结 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 210

    小徐
  • 提升10倍生产力:IDEA远程一键部署SpringBoot到Docker

    IDEA是Java开发利器,springboot是Java生态中最流行的微服务框架,docker是时下最火的容器技术,那么它们结合在一起会产生什么化学反应呢?

    Java团长
  • 使用Flask部署ML模型

    https://github.com/schmidtbri/using-ml-model-abc?source=post_page---------------...

    代码医生工作室
  • rabbitMQ系列高级整合应用Spring AMQP

    在Rabbit基础的API中,声明一个exchange、声明一个绑定、声明一个队列都是通过channel创建出来的。如下图:

    凯哥Java
  • 《快学BigData》--Hadoop总结(I)(42)

    Hadoop总结 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 210

    小徐
  • js基础-关于call,apply,bind的一切

    函数原型链中的 apply,call 和 bind 方法是 JavaScript 中相当重要的概念,与 this 关键字密切相关,相当一部分人对它们的理解还是比...

    OBKoro1
  • 匹马行天下——没有无缘无故的爱和恨,没有无缘无故的编程

    想这世间,没有无缘无故的爱,也没有无缘无故的恨,一切都有有原因的,我想编程亦是如此,技术时常更新,程序员时常学习,随着时间的推移,程序员发际线的增高,生活无处不...

    泰斗贤若如
  • Confluence 6 图片文件

    当你对一个页面进行编辑的时候,选择一个图片,将会显示图片属性面板。这个面板允许你设置显示大小,添加边控和特效和链接这个图片到其他页面。

    HoneyMoose
  • 性能分析之激情的过程无奈的结局

    我也没见过说负载高,cpu使用率不高的。因为load average本来就是cpu队列,cpu使用率不高就出现队列,也不是没可能,要是同步对象争用什么的。也应该...

    高楼Zee

扫码关注云+社区

领取腾讯云代金券