• GIL全局解释器锁


    GIL全局解释器锁

    一、引入:

    首先要明白,GIL并不是Python的一个特性,其实在我们通常所称呼的Python解释器,其实是CPython解释器,因为大部分Python程序都是基于该解释器执行的,当然还有JPython解释器(基于Java编写的),而这个GIL则是CPython解释器的特性,而不是Python的特性。

    GIL全称:Global Interpreter Lock,官方解释

    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解释器种类有哪些?

    1、CPython

    当从Python官方网站下载并安装好Python2.7后,就间接获得了一个官方版本的解释器:Cpython,这个解释器是用C语言开发的,所以叫CPython,在命名行下运行python,就是开始CPython解释器,CPython是使用尤其广的Python解释器。

    2、IPython

    IPython是基于CPython之上的一个交互式解释器,也就是说,IPython只是在交互方式上有所增强,不过执行Python代码的功能和CPython是完全一样的,好比特别多国产浏览器虽然外观不同,不过内核其实是调用了IE。

    3、PyPy

    PyPy是另一个Python解释器,它的目标是执行速度,PyPy采用JIT技术,对Python代码做到动态编译,所以不妨显著提高Python代码的执行速度。

    4、Jython

    Jython是运行在Java途径上的Python解释器,不妨间接把Python代码编译成Java字节码执行。

    5、IronPython

    IronPython和Jython相似,只不过IronPython是运行在微软.Net途径上的Python解释器,不妨间接把Python代码编译成.Net的字节码。

    综上所述,是有关python的五种常用的编程器,在Python的解释器中,使用广泛的是CPython,对于Python的编译

    三、GIL介绍

    GIL本质上也是一把互斥锁,既然是互斥锁,本质上都是将并发变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

    GIL达到的效果则是,同一时间内只有一个线程能够拿到GIL锁,拿到GIL锁后,该线程就可以使用解释器进行操作。

    用一个小例子说明GIL的小部分作用:

    没有GIL锁的话,如果有两个线程,一个线程是给变量添加数据,另一个线程则是垃圾回收机制的线程,此时可能就会出现一个问题,一个线程在定义数据产生后,还没有来的及将该数据的内存地址绑定给变量,就被垃圾回收机制给回收了(因为检测到这个数据计数为0),由于都是同一进程下的线程,且还是并发执行的,如果真的是这样执行,将给我们的程序造成很大的隐患。

    四、GIL与Lock

    可以存在的疑惑,既然有了GIL锁来保证同一时间只能有一个线程运行,那还需要Lock锁干嘛?

    首先,我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

    然后,我们可以得出结论:保护不同的数据就应该加不同的锁。

    最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

    GIL保护的是解释器级别的数据,保护用户自己的数据则需要自己加锁处理,如下图:

    在这里插入图片描述
    再来分析我们的疑问:

    1. 如果现在存在100个线程,那么其中某一个线程会抢到GIL锁,我们且称它为:线程1。
    2. 当线程1拿到GIL可以使用解释器后,线程1使用一个函数对全局变量count进行+1操作之前进行了互斥锁Lock。
    3. 而此时线程1还未执行完+1操作,线程2就把GIL锁给抢走了,此时线程2拿到这个GIL也想来执行这个+1操作,但执行这个函数时,发现有一个Lock未被释放,那它只能阻塞住,被释放GIL。
    4. 当线程1再次抢到这个GIL后,继续在上一次暂停的位置完成的+1操作,做完以后,再把Lock、GIL给释放掉。此后其它线程重复1、2、3、4步骤

    保护不同的数据就需要加不同的锁

    五、GIL与多线程

    有了GIL的存在,同一时刻同一进程中只有一个线程被执行

    创建进程开销大、而线程开销小,却无法利用多核优势,Python不行了?
    要解决这个问题,我们需要在几个点上达成一致:

    1. cpu到底是用来做计算的,还是用来做I/O的?
    2. 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能
    3. 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处

    所以我们Python中,多线程是并发操作的,而多进程是可以做到并行操作的。

    那么在Python里遇到I/O操作时,线程所用资源以及速度都会优于进程的。

    而如果计算的话,多进程则更占据优势

    代码示例:计算密集型,多进程效率更高

    from multiprocessing import Process
    from threading import Thread
    import os, time
    
    
    def work():
        res = 0
        for i in range(100000000):
            res *= i
    
    if __name__ == '__main__':
        l = []
        print(os.cpu_count())  # 本机为8核
        start = time.time()
        for i in range(4):
            p = Process(target=work)  # 多进程、耗时6s多
            # p = Thread(target=work)  # 多线程、耗时19s多
            l.append(p)
            p.start()
        for p in l:
            p.join()
        stop = time.time()
        print('run time is %s' % (stop - start))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    代码示例:I/O密集型,模拟I/O操作等待时间,线程并发效率更高

    from multiprocessing import Process
    from threading import Thread
    import os, time
    
    
    def work():
        time.sleep(4)
    
    
    if __name__ == '__main__':
        l = []
        print(os.cpu_count())  # 本机为8核
        start = time.time()
        for i in range(4):
            # p = Process(target=work)  # 多进程、耗时4.1s左右
            p = Thread(target=work)  # 多线程、耗时4.005s左右
            l.append(p)
            p.start()
        for p in l:
            p.join()
        stop = time.time()
        print('run time is %s' % (stop - start))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    因为开启进程的开销会远大于开启线程,而做这种I/O较多的操作,多线程会更具有优势

    应用场景举例:

    多线程用于IO密集型,如socket,爬虫,web
    多进程用于计算密集型,如金融分析

    总结

    1. 因为GIL的存在,只有IO较多场景下的多线程会得到较好的性能。
    2. 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者使用多进程实现。
    3. GIL在较长一段时间内将会继续存在,但是会不断对其进行改进。
  • 相关阅读:
    【强化学习论文合集 | 2018年合集】一. ICML-2018 强化学习论文
    基于PaddlePaddle的工业表计数环境搭建
    JavaScript基本数据类型
    JDBC编程
    SAP BC 源代码搜索
    JavaScript函数高级应用
    MySQL高级语句
    记录小白第一次EDUsrc:任意密码漏洞
    【数据结构】HashSet的底层数据结构
    leetcode 剑指 Offer 10- I. 斐波那契数列
  • 原文地址:https://blog.csdn.net/achen_m/article/details/133935074