前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python线程、协程探究(一)——Python多线程困境

Python线程、协程探究(一)——Python多线程困境

原创
作者头像
eedalong
修改2019-11-14 10:41:04
1.8K0
修改2019-11-14 10:41:04
举报

前两日帮同学解决的问题中涉及到python的线程、协程概念及其调度过程,加上之前总听说同学们去面试的时候会被问到python的多线程问题。所以想写一篇总结。本篇文章假定读者已经有一些操作系统知识的基础,并且几乎不涉及到具体编程,主要研究总结python独特的线程切换调度问题,以及最近用的越来越多的协程的概念和协程切换调度问题。

一、进程、线程概念回顾

操作系统课上我们都学过,进程是资源的分配单位,而线程是CPU调度运行的基本单位。也就是说,即使是多进程程序,调度依然是按照多个线程去进行调度,由于CPU时间片分配给每个独立调度的线程,拥有四个线程的进程比拥有一个线程的进程拥有更多的CPU时间片。如果一个有四个线程的进程运行在一个四核的CPU机器上,那么核的利用率可以达到100%,即所有的核都可以调度运行一个线程, 不会出现一方有难,八方围观的情况。同样,四个单线程进程也能使四核的CPU机器计算资源利用率达到100%,因为每个进程中的线程被独立调度执行。

一核有难,多核围观
一核有难,多核围观

二、CPython的多线程困境

当我们被问到python多线程的时候,回答一般都会涉及到GIL,但是GIL其实不是python本身的特性,而是CPython实现时引入的一种机制, 而JPython的实现里面里就没有GIL。这里我们主要研究CPython中由于GIL的存在而导致的独特的多线程困境,我们可以先看下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.)

GIL的存在本身就是为了阻止多个原生的线程同时执行python的字节码, 我们可以看下锁的实现数据结构

GIL锁数据结构
GIL锁数据结构

NRMUTEX中的thread_id就表明GIL锁目前被哪个thread拥有, 只有一个线程拥有了GIL锁,他才能被解释器解释执行,同一个python进程里面的其他线程就需要等待NRMUTEX的释放。这就会导致如下这个场景的问题出现:

python多线程困境
python多线程困境

比如一个拥有2个线程的python进程运行在2核的CPU上,我们假设每个线程都只涉及到纯CPU计算,不会被阻塞,只有线程运行的时间片到达才会进行线程切换,每个线程任务完成需要运行4s。我们编号2个线程为T1,T2, 编号2个核为C1,C2.如果是两个个非python线程, 是可以上做到上图所示的C1调度执行T1,C2调度执行T2, 2个线程并行执行,那么上述进程执行结束共需要4s。

但是由于CPython中GIL锁的存在,C1调度执行T1的时候,GIL锁被T1占着,T2拿不到GIL锁,处于阻塞的状态,等到T1执行结束或者执行的字节码行数到了设定的阈值,T1就会释放GIL锁,然后T2获得GIL锁之后再继续执行。这样的结果就是,这个拥有2个纯CPU计算线程的python程序进程运行结束需要8s,因为每个时刻,python进程中永远只有一个线程再被运行。那这就很胃疼了,这么看似乎python的多线程就没用了?也不是的, 上述情况下多线程没用,是因为我们假定的是每个线程运行代码都是纯CPU计算过程,不会遇到IO等阻塞操作,只在执行结束或者“轮转时间片”到了之后才会被切换,( 之所以打引号,是因为python的多线程调度的轮转时间片并不是常规CPU时间片,而是按照字节码来算的)。但是如果T1线程有IO操作会被阻塞,会在IO操作前提前释放GIL锁,进而T2线程获得GIL,可以正常被CPU调度执行,这样Python程序进程仍然处于继续运行的状态,而不会像单线程的时候遇到IO会被阻塞等待。话虽如此,除了少部分高端玩家,大部分情况下,我们用python的多线程时,多线程只是发挥了类似于异步处理的功能,不但没有发挥出多线程的并行威力,反而还承受了多线程的高昂的切换开销以及应对复杂的锁同步的问题。

三、Talk is cheap, show me the code

这个例子我是从[1]中的文章直接拿过来的,觉得还比较好的能说明在计算密集的时候python的多线程切换开销的影响。my_counter()就是一个纯CPU计算代码段,不会被阻塞。当线程运行my_counter()的时候只有在线程结束或者线程轮转时间片到达之后才会释放GIL锁,进行线程切换。

顺序执行的单个子线程,运行时间10.5s
顺序执行的单个子线程,运行时间10.5s
并发执行的多线程,运行时间17s
并发执行的多线程,运行时间17s

我们可以看到,顺序执行的过程中,只有一个子线程在执行my_counter(), 主线程由于在等待子线程执行结束,所以每次获得GIL锁之后又会立马释放锁。运行时间为10.5s, 在第二个程序中,我们同时创建两个子线程,“同时运行”my_counter(),python程序进程运行过程中,会有三个线程被调度切换,两个子线程“同时运行”程序,时间非但没有缩短,反而长了近一倍,这就是python线程切换带来的开销。

这个例子中,我们看到频繁的线程切换开销还是很高昂的, 这样的话,我们就干脆用python的单线程好了,但是单线程进程运行过程中当线程被阻塞时任务就停滞了,有没有一种办法,既能让单线程进程即使运行到阻塞操作如读取文件时,线程能不被阻塞,继续完成一些其他的任务,同时还不用承担这么高昂的切换代价呢?有的,那就是协程该登场的时候了。

四、引用

[1] https://cloud.tencent.com/developer/article/1489753

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、进程、线程概念回顾
  • 二、CPython的多线程困境
  • 三、Talk is cheap, show me the code
  • 四、引用
相关产品与服务
大数据
全栈大数据产品,面向海量数据场景,帮助您 “智理无数,心中有数”!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档