Python GIL

GIL(Global Interpreter Lock)

参考

UnderstandingGIL
Python的GIL是什么鬼,多线程性能究竟如何
JesseFang Python GIL
python 线程,GIL 和 ctypes
mutex与semaphore的区别
What is a mutex?

引文

GIL不是Python的特性,是在实现Python解释器CPython时所引入的一个概念。JPython就没有GIL。

官方解释

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.)
防止多线程并发执行机器码的一个mutex。全局排他锁。

只有获得了这个锁,才有在python解释器上执行python代码的权利。由于历史的原因这个锁的粒度太大了。

历史遗留原因

为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。

影响

全局锁的存在会对多线程的效率有不小的影响。几乎等于Python是个单线程的程序。

实验

使用一个循环一亿次的计数器函数。一个通过单线程执行2次,一个多线程执行。最后比较执行总时间。为了减少线程库本身性能损耗对测试结果带来的影响,这里单线程的代码同样使用了线程。只是顺序的执行两次,模拟单线程。

单线程
# singleThread.py
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: {}".format(end_time - start_time))

if __name__ == '__main__':
    main()

Total time: 19.9634678364

多线程
# mutiThread.py
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()
        thread_array[tid] = t
    for i in range(2):
        thread_array[i].join()
    end_time = time.time()
    print("Total time: {}".format(end_time - start_time))

if __name__ == '__main__':
    main()

Total time: 34.3221170902

GIL设计缺陷

使用操作系统本身的线程调度。通过操作系统调度算法进行调度。为了让各个线程能够平均利用CPU时间,Python会计算当前已执行的微代码数量,达到一定阈值后强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。

CPython进程作为一个整体,同一时间只会有一个获得了GIL的线程在跑,其他的线程都处于等待状态等着GIL的释放。因为GIL的限制,两个线程只是做着分时切换。

这种模式在只有一个CPU核心的情况下毫无问题。任何一个线程被唤起时都能成功获得GIL(因为只有释放了GIL才会引发线程调度)。但当CPU有多个核心时,问题就来了。从释放锁到获得锁几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经再一次获取到GIL了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着GIL欢快的执行着。然后达到切换时间后进入待调度状态,再被唤醒,再等待,如此往复恶性循环。

每次线程释放控制权后,其立马进入等待状态。而在多核情况下,往往由于拥有者比其它CPU运行的线程醒的更早,也就是自己释放后,立马自己又获得了。而其它CPU上运行的线程被唤醒时,发现锁还是被别人占着,空欢喜一场,无奈只能继续休眠等待。

IO密集型线程能否从多线程中受益呢?

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

解决方案

用多进程mutiprocess替代Thread

每个进程有自己独立的GIL,不会出现进程之间的GIL争抢。但它的引入会增加程序实现时进程间数据通讯和同步的困扰。

使用其他解释器

这个也不现实。其他解释器还是比较小众。

对并行计算性能较高的程序考虑把核心部分换成C模块
Python社区的努力

新版本中的改进

  • 将切换颗粒度从基于opcode技术改成基于时间片计数。
  • 避免最近一次释放GIL锁的线程在此被立即调度。
  • 新增线程优先级功能(高优先级线程可以迫使其他线程释放所持有的GIL锁)

相关概念

Mutex

When I am having a big heated discussion at work, I use a rubber chicken which I keep in my desk for just such occasions. The person holding the chicken is the only person who is allowed to talk. If you don’t hold the chicken you cannot speak. You can only indicate that you want the chicken and wait until you get it before you speak. Once you have finished speaking, you can hand the chicken back to the moderator who will hand it to the next person to speak. This ensures that people do not speak over each other, and also have their own space to talk.

Mutex和Semaphore

  • Mutex是一把钥匙,一个人拿了就可进入一个房间,出来的时候把钥匙交给队列的第一个。一般的用法是用于串行化对critical section代码的访问,保证这段代码不会被并行的运行。
  • Semaphore是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。对于N=1的情况,称为binary semaphore。一般的用法是,用于限制对于某一资源的同时访问。

总结

  1. GIL是一个全局锁。是CPython解释器特有的。
  2. 由于历史原因而产生的这个锁。
  3. 只有一个线程可以获得这个锁并运行。其他线程都得等待,所有的线程分时运行。
  4. 在多核多线程的情况下性能很差,因为锁的竞争。
  5. 可以使用多进程来解决。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值