zoukankan      html  css  js  c++  java
  • 全局解释器锁--GIL

    参考博客:https://www.cnblogs.com/mindsbook/archive/2009/10/15/thread-safety-and-GIL.html

            https://www.cnblogs.com/MnCu8261/p/6357633.html

         http://python.jobbole.com/87743/

    一、前言

      在多核cpu的背景下,基于多线程以充分利用硬件资源的编程方式也不断发展,也就是在同一时间,可以运行多个任务。但是Cpython中由于GIL的存在,导致同一时间内只有一个线程在运行。GIL的全称为Global Interpreter Lock,也就是全局解释器锁。存在在Python语言的主流执行环境Cpython中,GIL是一个真正的全局线程排他锁,在解释器执行任何Python代码时,都需要获得这把GIL锁。虽然 CPython 的线程库直接封装操作系统的原生线程,但 CPython 进程做为一个整体,同一时间只会有一个获得了 GIL 的线程在跑,其它的线程都处于等待状态等着 GIL 的释放。GIL 直接导致 CPython 不能利用物理多核的性能加速运算。

                

      不同的线程也是被分配到不同的核上面运行的,但是同一时间只有一个线程在运行

    二、为什么存在GIL

      2.1 线程安全

      想要利用多核的优势,我们可以采用多进程或者是多线程,两者的区别是资源是否共享。前者是独立的,而后者是共享的。相对于进程而言,多线程环境最大的问题是如果保证资源竞争、死锁、数据修改等。于是就有了线程安全。

       线程安全 是在多线程的环境下, 线程安全能够保证多个线程同时执行时程序依旧运行正确, 而且要保证对于共享的数据,可以由多个线程存取,但是同一时刻只能有一个线程进行存取.

      既然,多线程环境下必须存在资源的竞争,那么如何才能保证同一时刻只有一个线程对共享资源进行存取?

    加锁, 加锁可以保证存取操作的唯一性, 从而保证同一时刻只有一个线程对共享数据存取。

    通常加锁也有2种不同的粒度的锁:

    1. fine-grained(所谓的细粒度), 那么程序员需要自行地加,解锁来保证线程安全
    2. coarse-grained(所谓的粗粒度), 那么语言层面本身维护着一个全局的锁机制,用来保证线程安全

    前一种方式比较典型的是 java, Jython 等, 后一种方式比较典型的是 CPython (即Python)。

      2.2 Python自身特点

      依照Python自身的哲学, 简单 是一个很重要的原则,所以, 使用 GIL 也是很好理解的。多核 CPU 在 1990 年代还属于类科幻,Guido van Rossum 在创造 python 的时候,也想不到他的语言有一天会被用到很可能 多核的 CPU 上面,一个全局锁搞定多线程安全在那个时代应该是最简单经济的设计了。简单而又能满足需求,那就是合适的设计(对设计来说,应该只有合适与否,而没有好与不好)。

    三、线程切换

      一个线程无论何时开始睡眠或等待网络 I/O,其他线程总有机会获取 GIL 执行 Python 代码。这是协同式多任务处理。CPython 也还有抢占式多任务处理。如果一个线程不间断地在 Python 2 中运行 100次指令,或者不间断地在 Python 3 运行15 毫秒,那么它便会放弃 GIL,而其他线程可以运行。

      3.1 协同式多任务处理

      当一项任务比如网络 I/O启动,而在长的或不确定的时间,没有运行任何 Python 代码的需要,一个线程便会让出GIL,从而其他线程可以获取 GIL 而运行 Python。这种礼貌行为称为协同式多任务处理,它允许并发,多个线程同时等待不同事件。  

    def do_connect():
        s = socket.socket()
        s.connect(('python.org', 80))  # drop the GIL
     
    for i in range(2):
        t = threading.Thread(target=do_connect)
        t.start()
    

      两个线程在同一时刻只能有一个执行 Python ,但一旦线程开始连接,它就会放弃 GIL ,这样其他线程就可以运行。这意味着两个线程可以并发等待套接字连接,这是一件好事。在同样的时间内它们可以做更多的工作。

      3.2 抢占式多任务处理

      如果没有I/O中断,而是CPU密集型的的程序,解释器运行一段时间就会放弃GIL,而不需要经过正在执行代码的线程允许,这样其他线程便能运行。在python3中,这个时间间隔是15毫秒。

    四、Python中的线程安全

      如果一个线程可以随时失去 GIL,你必须使让代码线程安全。 然而 Python 程序员对线程安全的看法大不同于 C 或者 Java 程序员,因为许多 Python 操作是原子的。

      在列表中调用 sort(),就是原子操作的例子。线程不能在排序期间被打断,其他线程从来看不到列表排序的部分,也不会在列表排序之前看到过期的数据。原子操作简化了我们的生活,但也有意外。例如,+ = 似乎比 sort() 函数简单,但+ =不是原子操作。

      在python 2中(python3中结果没有问题):

    # -*- coding: UTF-8 -*-
    import time
    import threading
    
    n = 0
    
    
    def add_num():
        global n
        time.sleep(1)
        n += 1
    
    
    if __name__ == '__main__':
        thread_list = []
    
        for i in range(100):
            t = threading.Thread(target=add_num)
            t.start()
            thread_list.append(t)
    
        for t in thread_list:
            t.join()
    
        print 'final num:', n
    

      输出:

    [root@MySQL ~]# python mutex.py 
    final num: 98
    [root@MySQL ~]# python mutex.py 
    final num: 100
    [root@MySQL ~]# python mutex.py 
    final num: 96
    [root@MySQL ~]# python mutex.py 
    final num: 99
    [root@MySQL ~]# python mutex.py 
    final num: 100
    

      得到的结果本来应该是100,但是实际上并不一定。

      原因就在于,运行中有线程切换发生,一个线程失去了GIL,当一个线程A获取n = 43时,还没有完成n +=1这个操作,就失去了GIL,此时正好另一个线程B获取了GIL,并也获取了 n = 43,B完成操作后,n = 44。可是先前那个线程A又获得了GIL,又开始运行,最后也完成操作 n = 44。所有最后的结果就会出现偏差。

      

      上图就是n += 1运行到一半时失去GIL后又获得GIL的过程。

    五、Mutex互斥锁

      如何解决上面的偏差,保证结果的正确性?其实我们要做的就是确保每一次的运行过程是完整的,就是每次线程在获取GIL后,要将得到的共享数据计算完成后,再释放GIL锁。那又如何能做到这点呢?还是加锁,给运行的程序加锁,就能确保在程序运行时,必须完全运行完毕。  

    # -*- coding: UTF-8 -*-
    import time
    import threading
    
    n = 0
    lock = threading.Lock()    # 添加一个锁的实例
    
    def add_num():
        global n
        with lock:    # 获取锁
            n += 1
    
    
    if __name__ == '__main__':
        thread_list = []
    
        for i in range(100):
            t = threading.Thread(target=add_num)
            t.start()
            thread_list.append(t)
    
        for t in thread_list:
            t.join()        # 主线程等待所有线程执行完毕
    
        print 'final num:', n
    

      注:给程序加锁,程序就变成串行的了。所以程序中不能有sleep,同样数据量也不能特别大,否则会影响效率

  • 相关阅读:
    平衡——职场小说《监控》推荐
    《java程序员全攻略:从小工到专家》连载一:外行人眼中的IT人
    《java程序员全攻略:从小工到专家》连载二:IT行情分布
    各路技术牛人都推荐的书
    程序员2009精华本 有哪些值得期待
    揭秘孙小小《PPT演示之道》
    In the beginning, the world was void and without form…
    大学计算机课程复习操作系统
    大学计算机课程复习汇编语言
    (转)Winform 创建桌面快捷方式并开机启动
  • 原文地址:https://www.cnblogs.com/bigberg/p/7905221.html
Copyright © 2011-2022 走看看