Python 并发之GIL的限制

Python 并发之GIL的限制

GIL简介

我们首先要清楚的是GIL并不是Python的特性,它是实现Python解释器(CPython)时所引入的一个概念。

GIL并不是Python的特性,Python完全可以不依赖于GIL,GIL是一个防止解释器多线程并发执行机器码的一个全局互斥锁。其存在主要是因为在代码执行过程中,CPython的内存管理不是线程安全的。

为什么会有GIL

为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然是加锁,于是就有了GIL这个🔒,而当越来越多代码库开发者接受了这种设定后,他们开始大量依赖这种特性。

慢慢的这种实现方式被发现是低效而且让人难受的。但当大家视图去拆分和去除GIL的时候,发现大量库代码开发者已经严重依赖GIL而且难以去除了。

GIL的影响

GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小的影响。甚至就几乎等于Python是个单线程的程序。

下面我们就对比Python在多线程和单线程下得效率对比。测试方法很简单,一个循环1亿次的计数器函数。一个通过单线程执行两次,一个多线程执行。最后比较执行总时间。

顺序执行的单线程(single_thread.py)
# -*- coding: utf-8 -*-
# @File   : single_thread.py
# @Author : Runpeng Zhang
# @Date   : 2020/2/10
# @Desc   : 顺序执行的单线程


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: ', end_time - start_time)


if __name__ == '__main__':
    main()
同时执行的两个并发线程(multi_thread.py)
# -*- coding: utf-8 -*-
# @File   : single_thread.py
# @Author : Runpeng Zhang
# @Date   : 2020/2/10
# @Desc   : 顺序执行的单线程


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: ', end_time - start_time)


if __name__ == '__main__':
    main()
测试结果:

在这里插入图片描述
在这里插入图片描述

当前GIL设计的缺陷

基于pcode数量的调度方式

按照Python社区的想法,操作系统本身的线程调度已经非常成熟稳定了,没有必要自己搞一套。所以Python的线程就是C语言的一个pthread,并通过操作系统调度算法进行调度(例如linux是CFS)。为了让各个线程能够平均利用CPU时间,python会计算当前已执行的微代码数量,达到一定阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。

GIL的存在导致多线程无法很好的立即多核CPU的并发处理能力。

简单的总结下就是:Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。

如何避免受到GIL的影响

用multiprocessing替代Thread

multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。

当然multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。

用其他解析器

之前也提到了既然GIL只是CPython的产物,那么其他解析器是不是更好呢?没错,像JPython和IronPython这样的解析器由于实现语言的特性,他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。毕竟功能和性能大家在初期都会选择前者,Done is better than perfect。

所以没救了么?

当然Python社区也在非常努力的不断改进GIL,甚至是尝试去除GIL。并在各个小版本中有了不少的进步。有兴趣的读者可以扩展阅读这个Slide 另一个改进Reworking the GIL - 将切换颗粒度从基于opcode计数改成基于时间片计数 - 避免最近一次释放GIL锁的线程再次被立即调度 - 新增线程优先级功能(高优先级线程可以迫使其他线程释放所持有的GIL锁)

总结

Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。从本分的分析中,我们可以做以下一些简单的总结: - 因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能 - 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现 - GIL在较长一段时间内将会继续存在,但是会不断对其进行改进

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值