CPython的GIL

    GIL,  全称:  Global  Interpreter  Lock  (全局解释器锁),  是解释器采用的一种机制,
        它的作用是:确保同一时刻只有一个线程在执行。

回顾  操作系统、CPU、线程的关系

        线程包含需要  CPU  执行的指令集合,线程需要参加  操作系统的线程调度,才能获得CPU的使用时间,  即时间片。

GIL  的设计与运行原理

        1、线程创建:直接使用  ”操作系统调度算法“,  使用操作系统的原生线程。

        2、线程的切换  和  全局解释器锁:
              不能把  python线程  全权交给操作系统调度,因为操作系统是按时间片切换线程的,CPU只会机械地在时间片内执行
              一定量的指令,然而,一个  python对象  的基本操作(如赋值),包含的指令数量很可能比这要大,在一个时间片内,
              CPU很可能没法执行完。这就会导致线程安全问题。

              为此,作者设定了一些条件,使得一些  基本的对象操作,  能够保持其原子性。  那就是给整个解释器加一把锁,
              从而确保  每次只有一个线程能工作  ,只有在达成一定的条件时,才会释放锁,让其他线程工作

              可以想象成,把多个线程都关到一个屋子里,每次只能有一个线程出来玩,限定玩多久就必须回来,让其他线程出去玩

        3、释放锁的指定条件:
                条件一:  设置一个超时时间,在线程达到这个时间后,释放  GLI。
                              在  Python3.2之前,是通过计数实现(100个Python指令)
                或条件二:  执行  I/O  操作时总是会释放  GIL。因为  I/O  操作一般都很耗时,需要等待对方响应与其阻塞等待,
                                不如先让其他线程工作。(所以  GIL对于  IO密集型的多线程程序影响不大)

得益于GIL,能够实现原子性的基本操作

        引自  官方文档
        每一条字节码指令以及每一条指令对应的  C  代码实现都是原子的。
        而实际上,对内建类型(int,list,dict  等)的共享变量的“类原子”操作都是原子的。
        举例来说,下面的操作是原子的(L、L1、L2  是列表,D、D1、D2  是字典,x、y  是对象,i,j  是  int  变量):

L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

        以下操作不是原子操作,  如果在多线程环境其存在共享资源的情况下,必须使用互斥锁

i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1

        另外,覆盖其他对象的操作会在其他对象的引用计数变成  0  时触发其  del()  方法,
        这可能会产生一些影响。对字典和列表进行大量操作时尤其如此。如果有疑问的话,使用互斥锁!

GIL  的缺陷

        GIL  每次只允许一个线程出去参加  "操作系统的线程调度",在  ”单核处理器时代“  并没有什么缺陷,因为在当时,
        多线程仅仅是用于提高资源利用率(不让CPU闲着),并不会提高单个程序的计算效率,  所以,GIL  的设计  完全符合当时的
        需求,  能够让多线程很高效地使用单核CPU。

        到了  ”多核处理器时代“,多线程程序实际上可以使用多核来提高计算效率,  但  Python  因为  GIL  的存在,即使在多核
        处理器下,依然每次只放出一个线程工作,这样就导致程序至始至终都只用到1个核,完全没有发挥出多核处理器的性能,
        就计算效率而言,还不如单线程程序效率高。

        官方尝试过优化甚至是删除GIL。优化方案没有成功,因为新的GIL严重降低了单线程的性能。另外,删除方案也很难实现,
        因为当前已经有太多库依赖着GIL的特性了。

        那么,其他语言,如  Java  是怎么让多线程使用多核且保证程安全的呢?
                不像  Python  把一部分工作放到了解释器里,Java  直接把线程安全工作都堆到了用户层面:
                1、和  Python一样实现了悲观互斥锁
                2、官方实现了无阻塞乐观锁:提供了很多atomic类,利用  CPU硬件原语  CAS  实现。

GIL缺陷  解决方案

        1、使用多进程:  每个进程都有自己的GIL,只要分成多进程,就能有效地使用多核了。
                concurrent.futures模块中的  ProcessPoolExecutor类提供了一个简单的方法,如果想对任务分发做更多控制,
                可以使用  multiprocessing  模块提供的底层  API。
        2、使用C扩展:  可使用  C  拓展处理耗时较久的任务,在调用  C  代码时,释放  GIL,让其他线程执行。
                zlib,  hashlib  等标准库就是这样做的。
        3、使用协程:  针对单线程IO密集型程序而言,io等待时间较长,可以先挂起io,先执行线程中的其他逻辑,
                等收到io数据写到内存时,再处理这逻辑。

参考文档:

    知乎:  浅析CPython的全局解释锁GIL
    Python官方文档:
        Thread  State  and  the  Global  Interpreter  Lock
        What  kinds  of  global  value  mutation  are  thread-safe?