热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

cond.nopython计算_Python进阶:深入GIL(下篇)

Python进阶:深入GIL(下篇)HackPython致力于有趣有价值的编程教学简介有朋友吐槽,文章中太多表情,其实我加表情的初衷是避免

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

  1. /* A pthread mutex isn't sufficient to model the Python lock type
  2. * because, according to Draft 5 of the docs (P1003.4a/D5), both of the
  3. * following are undefined:
  4. * -> a thread tries to lock a mutex it already has locked
  5. * -> a thread tries to unlock a mutex locked by a different thread
  6. * pthread mutexes are designed for serializing threads over short pieces
  7. * of code anyway, so wouldn't be an appropriate implementation of
  8. * Python's locks regardless.
  9. *
  10. * The pthread_lock struct implements a Python lock as a "locked?" bit
  11. * and a pair. In general, if the bit can be acquired
  12. * instantly, it is, else the pair is used to block the thread until the
  13. * bit is cleared. 9 May 1994 tim@ksr.com
  14. */
  15. # GIL的定义
  16. typedef struct {
  17. char locked; /* 0=unlocked, 1=locked */
  18. /* a pair to handle an acquire of a locked lock */
  19. pthread_cond_t lock_released;
  20. pthread_mutex_t mut;
  21. } 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()方法来实现线程对锁的获取与释放,先来看一下获取,代码如下:

  1. PyLockStatus
  2. PyThread_acquire_lock_timed(PyThread_type_lock lock, PY_TIMEOUT_T microseconds,
  3. int intr_flag)
  4. {
  5. PyLockStatus success = PY_LOCK_FAILURE;
  6. // GIL
  7. pthread_lock *thelock = (pthread_lock *)lock;
  8. int status, error = 0;
  9. dprintf(("PyThread_acquire_lock_timed(%p, %lld, %d) calledn",
  10. lock, microseconds, intr_flag));
  11. if (microseconds == 0) {
  12. // 获取互斥锁,从而让当前线程获得操作locked变量的权限
  13. status = pthread_mutex_trylock( &thelock->mut );
  14. if (status != EBUSY)
  15. CHECK_STATUS_PTHREAD("pthread_mutex_trylock[1]");
  16. }
  17. else {
  18. // 获取互斥锁,从而让当前线程获得操作locked变量的权限
  19. status = pthread_mutex_lock( &thelock->mut );
  20. CHECK_STATUS_PTHREAD("pthread_mutex_lock[1]");
  21. }
  22. if (status == 0) {
  23. if (thelock->locked == 0) {
  24. // 获得锁
  25. success = PY_LOCK_ACQUIRED;
  26. }
  27. else if (microseconds != 0) {
  28. struct timespec ts; // 时间
  29. if (microseconds > 0)
  30. // 等待事件
  31. MICROSECONDS_TO_TIMESPEC(microseconds, ts);
  32. /* 继续尝试,直到我们获得锁定 */
  33. //mut(互斥锁) 必须被当前线程锁定
  34. // 获得互斥锁失败,则一直尝试
  35. while (success == PY_LOCK_FAILURE) {
  36. if (microseconds > 0) {
  37. // 计时等待持有锁的线程释放锁
  38. status = pthread_cond_timedwait(
  39. &thelock->lock_released,
  40. &thelock->mut, &ts);
  41. if (status == ETIMEDOUT)
  42. break;
  43. CHECK_STATUS_PTHREAD("pthread_cond_timed_wait");
  44. }
  45. else {
  46. // 无条件等待持有锁的线程释放锁
  47. status = pthread_cond_wait(
  48. &thelock->lock_released,
  49. &thelock->mut);
  50. CHECK_STATUS_PTHREAD("pthread_cond_wait");
  51. }
  52. if (intr_flag && status == 0 && thelock->locked) {
  53. // 被唤醒了,但没有锁,则设置状态为PY_LOCK_INTR 当做异常状态来处理
  54. success = PY_LOCK_INTR;
  55. break;
  56. }
  57. else if (status == 0 && !thelock->locked) {
  58. success = PY_LOCK_ACQUIRED;
  59. }
  60. }
  61. }
  62. // 获得锁,则当前线程上说
  63. if (success == PY_LOCK_ACQUIRED) thelock->locked = 1;
  64. // 释放互斥锁,让其他线上有机会竞争获得锁
  65. status = pthread_mutex_unlock( &thelock->mut );
  66. CHECK_STATUS_PTHREAD("pthread_mutex_unlock[1]");
  67. }
  68. if (error) success = PY_LOCK_FAILURE;
  69. dprintf(("PyThread_acquire_lock_timed(%p, %lld, %d) -> %dn",
  70. lock, microseconds, intr_flag, success));
  71. return success;
  72. }
  73. int
  74. PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)
  75. {
  76. return PyThread_acquire_lock_timed(lock, waitflag ? -1 : 0, /*intr_flag=*/0);
  77. }

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

  1. // 获得互斥锁
  2. pthread_mutex_lock(pthread_mutex_t *mutex);
  3. // 获得互斥锁
  4. pthread_mutex_trylock(pthread_mutex_t *mutex);
  5. // 释放互斥锁
  6. 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()方法,代码如下:

  1. void
  2. PyThread_release_lock(PyThread_type_lock lock)
  3. {
  4. pthread_lock *thelock = (pthread_lock *)lock;
  5. int status, error = 0;
  6. (void) error; /* silence unused-but-set-variable warning */
  7. dprintf(("PyThread_release_lock(%p) calledn", lock));
  8. // 获取互斥锁,从而让当前线程操作locked变量的权限
  9. status = pthread_mutex_lock( &thelock->mut );
  10. CHECK_STATUS_PTHREAD("pthread_mutex_lock[3]");
  11. // 释放GIL,将locked置为0
  12. thelock->locked = 0;
  13. /* wake up someone (anyone, if any) waiting on the lock */
  14. // 通知其他线程当前线程已经释放GIL
  15. status = pthread_cond_signal( &thelock->lock_released );
  16. CHECK_STATUS_PTHREAD("pthread_cond_signal");
  17. // 释放互斥锁
  18. status = pthread_mutex_unlock( &thelock->mut );
  19. CHECK_STATUS_PTHREAD("pthread_mutex_unlock[3]");
  20. }

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。

f706d85c15987fb5260898f1cb414ae0.png

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

45058ed4e51c0162cc4671c2d25279bf.png

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

  1. def count(n):
  2. while n > 0:
  3. n -= 1

顺序执行

  1. count(100000000)
  2. count(100000000)

耗时24.6s

多线程运行

  1. t1 = Thread(target=count,args=(100000000,))
  2. t1.start()
  3. t2 = Thread(target=count,args=(100000000,))
  4. 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是挂起状态。

05193a2788cc86c46427c1fac3f04291.png

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

ab6f565b95009f78f127687821264a75.png

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

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

f43ca280c6a20a30094a260a76680cdf.png

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

40843ea3d70279774324b351a606827d.png

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

8bca342a6077c33430f16066631a6ce2.png

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

49853bbc986151c7d9c8f2746eaf483b.png

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

6e7ed00bbb4a418fdce8999892c18459.png

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

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

0199d17e1d0c59883bca8a50e25b7e22.png

图中,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 的教学课程并感觉您的阅读与支持。

参考文章:

  • NewGIL
  • GIL 的实现细节



推荐阅读
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • GPT-3发布,动动手指就能自动生成代码的神器来了!
    近日,OpenAI发布了最新的NLP模型GPT-3,该模型在GitHub趋势榜上名列前茅。GPT-3使用的数据集容量达到45TB,参数个数高达1750亿,训练好的模型需要700G的硬盘空间来存储。一位开发者根据GPT-3模型上线了一个名为debuid的网站,用户只需用英语描述需求,前端代码就能自动生成。这个神奇的功能让许多程序员感到惊讶。去年,OpenAI在与世界冠军OG战队的表演赛中展示了他们的强化学习模型,在限定条件下以2:0完胜人类冠军。 ... [详细]
  • 2016 linux发行版排行_灵越7590 安装 linux (manjarognome)
    RT之前做了一次灵越7590黑苹果炒作业的文章,希望能够分享给更多不想折腾的人。kawauso:教你如何给灵越7590黑苹果抄作业​zhuanlan.z ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 统一知识图谱学习和建议:更好地理解用户偏好
    本文介绍了一种将知识图谱纳入推荐系统的方法,以提高推荐的准确性和可解释性。与现有方法不同的是,本方法考虑了知识图谱的不完整性,并在知识图谱中传输关系信息,以更好地理解用户的偏好。通过大量实验,验证了本方法在推荐任务和知识图谱完成任务上的优势。 ... [详细]
  • 实现一个通讯录系统,可添加、删除、修改、查找、显示、清空、排序通讯录信息
    本文介绍了如何实现一个通讯录系统,该系统可以实现添加、删除、修改、查找、显示、清空、排序通讯录信息的功能。通过定义结构体LINK和PEOPLE来存储通讯录信息,使用相关函数来实现各项功能。详细介绍了每个功能的实现方法。 ... [详细]
  • Gitlab接入公司内部单点登录的安装和配置教程
    本文介绍了如何将公司内部的Gitlab系统接入单点登录服务,并提供了安装和配置的详细教程。通过使用oauth2协议,将原有的各子系统的独立登录统一迁移至单点登录。文章包括Gitlab的安装环境、版本号、编辑配置文件的步骤,并解决了在迁移过程中可能遇到的问题。 ... [详细]
author-avatar
婉里去_
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有