Python进阶:深入GIL(下篇)HackPython致力于有趣有价值的编程教学

简介

有朋友吐槽,文章中太多表情,其实我加表情的初衷是避免大家阅读疲劳,既然造成了反效果,后面的内容就不会在添加表情了。

在上一篇GIL的文章中,感性的了解了GIL,本篇文章尝试从源码层面来简单解析一下GIL,这里使用cpython 3.7版本的源码(其实这块没有太大的改变,所以你看3.5、3.6的Python源码都可以),你可以直接通过github浏览相关部分的源码。

GIL的定义

因为Python线程使用了操作系统的原生线程,这导致了多个线程同时执行容易出现竞争状态等问题,为了方便Python语言层面开发者的开发,就使用了GIL(Global Interpreter Lock)这个大锁,一口气锁住,这样开发起来就方便了,但也造成了当下Python运行速度慢的问题。

有人感觉GIL锁其实就是一个互斥锁(Mutex lock),其实不然,GIL的目的是让多个线程按照一定的顺序并发执行,而不是简单的保证当下时刻只有一个线程运行,这点CPython中也有相应的注释,而且就是在GIL定义之上,具体如下:

源码路径:Python/thread_pthread.h/* A pthread mutex isn't sufficient to model the Python lock type

* because, according to Draft 5 of the docs (P1003.4a/D5), both of the

* following are undefined:

* -> a thread tries to lock a mutex it already has locked

* -> a thread tries to unlock a mutex locked by a different thread

* pthread mutexes are designed for serializing threads over short pieces

* of code anyway, so wouldn't be an appropriate implementation of

* Python's locks regardless.

*

* The pthread_lock struct implements a Python lock as a "locked?" bit

* and a pair. In general, if the bit can be acquired

* instantly, it is, else the pair is used to block the thread until the

* bit is cleared. 9 May 1994 tim@ksr.com

*/

# GIL的定义

typedef struct {

char locked; /* 0=unlocked, 1=locked */

/* a pair to handle an acquire of a locked lock */

pthread_cond_t lock_released;

pthread_mutex_t mut;

} pthread_lock;

从GIL的定义中可知,GIL本质是一个条件互斥组(),其使用条件变量lock_released与互斥锁mut来保护locked的状态,locked为0时表示未上锁,为1时表示线程上锁,而条件变量的引用让GIL可以实现多个线程按一定条件并发执行的目的。

条件变量(condition variable)是利用线程间共享的全局变量来控制多个线程同步的一种机制,其主要包含两个动作:

1.一个线程等待「条件变量的条件成立」而挂起 2.另一个线程则是「条件成功」(即发出条件成立的信号)

在很多系统中,条件变量通常与互斥锁一同使用,目的是确保多个操作的原子性从而避免死锁的发生。

GIL的获取与释放

从GIL的定义结构可以看出,线程对GIL的操作其实就是修过GIL结构中的locked变量的状态来达到获取或释放GIL的目的,在Python/threadpthread.h中以及提供了PyThreadacquirelock()与PyThreadrelease_lock()方法来实现线程对锁的获取与释放,先来看一下获取,代码如下:PyLockStatus

PyThread_acquire_lock_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds,

int intr_flag)

{

PyLockStatus success = PY_LOCK_FAILURE;

// GIL

pthread_lock *thelock = (pthread_lock *)lock;

int status, error = 0;

dprintf(("PyThread_acquire_lock_timed(%p, %lld, %d) called\n",

lock, microseconds, intr_flag));

if (microseconds == 0) {

// 获取互斥锁,从而让当前线程获得操作locked变量的权限

status = pthread_mutex_trylock( &thelock->mut );

if (status != EBUSY)

CHECK_STATUS_PTHREAD("pthread_mutex_trylock[1]");

}

else {

// 获取互斥锁,从而让当前线程获得操作locked变量的权限

status = pthread_mutex_lock( &thelock->mut );

CHECK_STATUS_PTHREAD("pthread_mutex_lock[1]");

}

if (status == 0) {

if (thelock->locked == 0) {

// 获得锁

success = PY_LOCK_ACQUIRED;

}

else if (microseconds != 0) {

struct timespec ts; // 时间

if (microseconds > 0)

// 等待事件

MICROSECONDS_TO_TIMESPEC(microseconds, ts);

/* 继续尝试,直到我们获得锁定 */

//mut(互斥锁) 必须被当前线程锁定

// 获得互斥锁失败,则一直尝试

while (success == PY_LOCK_FAILURE) {

if (microseconds > 0) {

// 计时等待持有锁的线程释放锁

status = pthread_cond_timedwait(

&thelock->lock_released,

&thelock->mut, &ts);

if (status == ETIMEDOUT)

break;

CHECK_STATUS_PTHREAD("pthread_cond_timed_wait");

}

else {

// 无条件等待持有锁的线程释放锁

status = pthread_cond_wait(

&thelock->lock_released,

&thelock->mut);

CHECK_STATUS_PTHREAD("pthread_cond_wait");

}

if (intr_flag && status == 0 && thelock->locked) {

// 被唤醒了,但没有锁,则设置状态为PY_LOCK_INTR 当做异常状态来处理

success = PY_LOCK_INTR;

break;

}

else if (status == 0 && !thelock->locked) {

success = PY_LOCK_ACQUIRED;

}

}

}

// 获得锁,则当前线程上说

if (success == PY_LOCK_ACQUIRED) thelock->locked = 1;

// 释放互斥锁,让其他线上有机会竞争获得锁

status = pthread_mutex_unlock( &thelock->mut );

CHECK_STATUS_PTHREAD("pthread_mutex_unlock[1]");

}

if (error) success = PY_LOCK_FAILURE;

dprintf(("PyThread_acquire_lock_timed(%p, %lld, %d) -> %d\n",

lock, microseconds, intr_flag, success));

return success;

}

int

PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)

{

return PyThread_acquire_lock_timed(lock, waitflag ? -1 : 0, /*intr_flag=*/0);

}

上述代码中使用了下面3个方法来操作互斥锁// 获得互斥锁

pthread_mutex_lock(pthread_mutex_t *mutex);

// 获得互斥锁

pthread_mutex_trylock(pthread_mutex_t *mutex);

// 释放互斥锁

pthread_mutex_unlock(pthread_mutex_t *mutex);

这些方法会操作POSIX线程(POSIX thread,简称Pthread)去操作锁,在Linux、MacOS等类Unix操作系统中都会使用Pthread作为操作系统的线程,这3个方法具体的细节不是本章主题,不再细究。

从上诉代码中可以看出,获取GIL锁的逻辑主要在PyThreadacquirelock_timed()方法中,其主要的逻辑为,如果没有获得锁,就等待,具体分为计算等待与无条件等待,与Python2不同,Python3通过计时的方式来触发「检查间隔」(check interval)机制,直到成功获取GIL,具体逻辑可以看代码中注释。

接着来看是否GIL锁的逻辑,即PyThreadreleaselock()方法,代码如下:void

PyThread_release_lock(PyThread_type_lock lock)

{

pthread_lock *thelock = (pthread_lock *)lock;

int status, error = 0;

(void) error; /* silence unused-but-set-variable warning */

dprintf(("PyThread_release_lock(%p) called\n", lock));

// 获取互斥锁,从而让当前线程操作locked变量的权限

status = pthread_mutex_lock( &thelock->mut );

CHECK_STATUS_PTHREAD("pthread_mutex_lock[3]");

// 释放GIL,将locked置为0

thelock->locked = 0;

/* wake up someone (anyone, if any) waiting on the lock */

// 通知其他线程当前线程已经释放GIL

status = pthread_cond_signal( &thelock->lock_released );

CHECK_STATUS_PTHREAD("pthread_cond_signal");

// 释放互斥锁

status = pthread_mutex_unlock( &thelock->mut );

CHECK_STATUS_PTHREAD("pthread_mutex_unlock[3]");

}

PyThreadreleaselock()方法的逻辑相对简洁,首先获取互斥锁,从而拥有操作locked的权限,然后就将locked置为0,表示释放GIL,接着通过pthreadcondsignal()方法通知其他线程「当前线程已经释放GIL」,让其他线程去获取GIL,其他线程其实就是在调用pthreadcondtimedwait()方法或pthreadcondwait()方法等待的线程。

改进后GIL的优势

通过前面内容的讨论,已经知道Python3.x中并没有取消GIL,而是将其改进,让它变得更好一些。(具体而言Python3.2中对GIL进行了改进),改进后的GIL相比旧GIL(Python2.x)会让线程对GIL的竞争更加平稳,下图是旧GIL在2个CPU下2个线程之间运行状态,可以发现就GIL中存在这大佬的Failed GIL Acquire。

究其原因,是因为旧GIL基于ticker来决定是否释放GIL(ticker默认为100),并且释放完后,释放的线程依旧会参与GIL争夺,这就使得某线程一释放GIL就立刻去获得它,而其他CPU核下的线程相当于白白被唤醒,没有抢到GIL后,继续挂起等待,这就造成了资源的浪费,形象如下图:

写一段简单的测试旧GIL造成的影响,在 双核2Ghz Macbook OS-X 10.5.6下运行def count(n):

while n > 0:

n -= 1

顺序执行count(100000000)

count(100000000)

耗时24.6s

多线程运行t1 = Thread(target=count,args=(100000000,))

t1.start()

t2 = Thread(target=count,args=(100000000,))

t2.start()

耗时45.5s,满了接近1.8倍,如果你在单核上运行,则耗时38.0s,依旧比顺序执行慢,造成这么大的差距,就是因为旧GIL本身的设计存在问题,在多线程争夺GIL时有大量的资源消耗。

而改进后的GIL不再使用ticker,而改为使用时间,可以通过 sys.getswitchinterval()来查看GIL释放的时间,默认为5毫秒,此外虽然说新GIL使用了时间,但决定线程是否释放GIL并不取决于时间,而是取决于gildroprequest这一全局变量,如果gildroprequest=0,则线程会在解释器中一直运行,直到gildroprequest=1,此时线程才会释放GIL,下面同样以两个线程来解释新GIL在其中发挥的具体作用。

首先存在两个线程,Thread 1是正在运行的状态,Thread 2是挂起状态。

Thread 2之所以挂起,是因为Thread 2没有获得GIL,它会执行cv_wait(gil,TIMEOUT)定时等待方法,等待一段时间(默认5毫秒),直到Thread 1主动释放GIL(比如Thread 1 执行I/O操作时会进入休眠状态,此时它会主动释放GIL)。

当Thread 2手动signal信号后,就知道Thread 1要休眠了,此时它就可以去获取GIL从而执行自身的逻辑。

另外一种情况就是,Thread 1一直在执行,执行的时间超过了Thread 2 cvwait(gil,TIMEOUT)方法等待的时间,此时Thread 2就会去修改全局变量gildroprequest,将其设置为1,然后自己再次调用cvwait(gil,TIMEOUT)挂起等待。

Thread 1 发现 gildroprequest=1 会主动释放GIL,并通过signal通知Thread 2,让其获取GIL去运行。

其中需要注意的细节如下图。当Thread 1因为gildroprequest=1要主动释放GIL后,会调用cv_wait(gotgil)方法进入等待状态,该状态下的Thread 1会等待Thread 2返回的signal信号,从而得知另一个线程(Thread 2)成功获得了GIL并在执行状态,这就避免了多个线程争夺GIL的情况,从而避免了额外资源的消耗。

然后相同的过程会重复的发生,直到线程执行结束

如果存在多个线程(大于2个线程),此时多个线程出现等待时间超时,此时会不会发生多个线程争夺GIL的情况呢?答案是不会,如下图:

当Thread 1执行时,Thread 2等待超时了,会设置gildroprequest = 1,从而让Thread 2获得运行权限,如果此时Thread 3或Thread 4一会后也超时了,此时是不会让Thread 2将获得的GIL立即释放的,Thread 3/4 会继续在挂起状态等待一段时间。

还需要注意的一点是,设置gildroprequest=1的线程并不一定会是下一个要执行的线程,下一个要执行那个线程,这取决于操作系统,直观理解如下图:

图中,Thread 2到了超时时间,将gildroprequest设置为了1,但Thread 1发送signal信号的线程是Thread 3,这造成Thread 2继续挂起等待,而Thread 3获得GIL执行自身逻辑。

改进后的GIL使用上面相同的测试代码在四核 MacPro, OS-X 10.6.2 下运行,其顺序执行时间与多线程运行时间不会有太大差距

顺序执行耗时:23.5s 双线程执行耗时:24.0s

可以看出改进后的GIL相比旧GIL已经有了比较大的性能提升。

结尾

本节从源码层面简单的讨论了GIL,欢迎学习 HackPython 的教学课程并感觉您的阅读与支持。

参考文章:

python 3.9 gil_Python进阶:深入GIL(下篇)相关推荐

  1. Python学习day13-函数进阶(1)

    Python学习day13-函数进阶(1) 闭包函数 闭包函数,从名字理解,闭即是关闭,也就是说把一个函数整个包起来.正规点说就是指函数内部的函数对外部作用域而非全局作用域的引用. 为函数传参的方式有 ...

  2. 八十八、Python | 十大排序算法系列(下篇)

    @Author:Runsen @Date:2020/7/10 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏 ...

  3. python解释器的工作原理_Python GIL全局解释器锁详解(深度剖析)

    通过前面的学习,我们了解了 Pyton 并发编程的特性以及什么是多线程编程.其实除此之外,Python 多线程还有一个很重要的知识点,就是本节要讲的 GIL. GIL,中文译为全局解释器锁.在讲解 G ...

  4. 深入理解Python中的全局解释锁GIL

    深入理解Python中的全局解释锁GIL 转自:https://zhuanlan.zhihu.com/p/75780308 注:本文为蜗牛学院资深讲师卿淳俊老师原创,首发自公众号https://mp. ...

  5. python快速入门及进阶

    python快速入门及进阶 by 小强 转载于:https://www.cnblogs.com/dingxiaoqiang/p/11387174.html

  6. python职业发展规划-Python开发者的四大进阶攻略,菜鸟的成神之路

    原标题:Python开发者的四大进阶攻略,菜鸟的成神之路 随着人工智能的发展与应用,Python编程语言受到世界各界人士的关注,编程圈金句从"人生苦短,我学Python"转变成了& ...

  7. Python 数据挖掘与机器学习进阶实训-3-韦玮-专题视频课程

    Python 数据挖掘与机器学习进阶实训-3-106人已学习 课程介绍         Python 数据挖掘与机器学习进阶实训-3 课程收益     培养Python全栈工程师 讲师介绍     韦 ...

  8. Python 数据挖掘与机器学习进阶实训-2-韦玮-专题视频课程

    Python 数据挖掘与机器学习进阶实训-2-39人已学习 课程介绍         Python 数据挖掘与机器学习进阶实训-2 课程收益     培养Python全栈工程师 讲师介绍     韦玮 ...

  9. Python 数据挖掘与机器学习进阶实训-1-韦玮-专题视频课程

    Python 数据挖掘与机器学习进阶实训-1-262人已学习 课程介绍         Python 数据挖掘与机器学习进阶实训-1 课程收益     培养Python全栈工程师 讲师介绍     韦 ...

最新文章

  1. 【Android 插件化】VAHunt 检测插件化引擎的具体细节
  2. 出租司机给我上的MBA课 -- [ 来自: ] [作者:cexo255]
  3. 循环结果添加到集合_Java Note-数据结构(4)集合
  4. 牛客练习赛74 E CCA的期望(算概率的技巧+floyd处理)
  5. 网页mp3提取器_用Python写一个酷狗音乐下载器!
  6. 设计模式学习笔记——中介者(Mediator)模式
  7. 在线生成艺术字_生成艺术:如何修改绘画
  8. Qt4_基于项的图形视图
  9. Hibernate持久化对象的三种状态深入理解
  10. php 时间转换时间戳_PHP日期格式转时间戳
  11. 沉迷游戏在心理学怎么解释
  12. C# 将Big5繁体转换简体GB2312的代码
  13. Inverting Visual Representations with Convolutional Networks
  14. 【企业开源】小米开源:站在巨人肩膀上的创新
  15. 计算机电子邮箱格式,英语邮箱格式,电子邮件格式范文!
  16. 如何加速./mk snod打包
  17. 苹果可穿戴设备项目背后的那些专家
  18. Map.containsKey() 的一个使用场景.
  19. 【Machine Learning】梯度下降算法介绍_02
  20. Kotlin 编码规约

热门文章

  1. SEO泛目录快速排名技术
  2. STATA如何对变量进行排序
  3. mybatis 插件查询指定列
  4. 前锋PHP咋样,最新说说燃气热水器前锋热水器A系列真的好吗,究竟怎么样呢?达人深度全面评测...
  5. 我与《深入浅出嵌入式底层软件开发》
  6. html 画廊代码,HTML5实践-使用css装饰图片画廊的代码分享(二)
  7. 怎么做表情包微信gif?表情包制作软件分享!​
  8. Android AudioTrack播放PCM文件
  9. 荣耀出厂是Android6,华为荣耀6plus怎么恢复出厂设置让系统恢复初始
  10. 【电子学会】2021年12月图形化四级 -- 聪明的小猫