全局解释器锁--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,同样数据量也不能特别大,否则会影响效率

 

posted @ 2017-11-27 17:36  Bigberg  阅读(722)  评论(0编辑  收藏  举报