在python
面试里,多线程是避不开的话题,其中一个经典问题就是,多线程threading.Thread
是不是并行运行。这个问题的答案,说是也不对,说不是也不对,得分情况讨论。本文就带领大家,分析并回答这个问题。
我们用一段代码来做实验:
1 | import subprocess |
这段代码意思很容易懂,起了5个testfunc
为target
的线程,然后主线程等待这些线程全部执行结束join
,最后统计每个线程的开始结束时间间隔和开始结束时间间隔。
执行了之后我们会发现如果test_func
里设置的是code_task
的话,那么总时间相当于一个线程执行时间的num_threads
倍,这个可以通过设置num_threads
为1以及更高的数字来实验到,而更为蹊跷的是,每个线程执行的时间间隔和总的执行时间间隔几乎相差无几。可以看下面的输出例子:
1 | # num_threads = 1 |
然后,如果test_func
里设置的是io_task
的话,num_threads
就算设置三五个,用的时间基本都一样:
1 | # num_threads = 3 |
所以两种情况下,一个是节省时间并行的,一个是没节省时间不并行的。这是为什么呢?
很多同学会提到GIL
这个概念,没错,GIL
就是关键。GIL
是全局解释器锁,它的意义就在于,单个python
运行时环境里,同一时刻,保证只有一个操作系统线程能够解释执行python
代码。python
多线程,实质是起了多个操作系统线程,竞争GIL
锁解释执行python
代码,在多线程执行code_task
的情况下,不同线程需要获取GIL
,才能执行while
、+=
之类的语句,所以这些线程全部执行完的话,总的时间是累加的。而在io_task
的情况下,等待ping
命令返回的期间是阻塞在操作系统的io
接口,并不需要解释执行python
代码,因此单个任务线程不需要每时每刻都拥有GIL
,各个线程间也没有什么竞争关系了,所以最后呈现了并行的状态。这就是上述问题的答案了。
当然,这个问题还有引申的空间——为什么code_task
的场景下,每个线程执行的时间间隔和总的执行时间间隔几乎相差无几,而不是线程一个接一个串行执行完毕呢?这是因为在python
运行时环境里GIL
的竞争是时分复用的,每个时间间隔sys.getswitchinterval()
都会触发一次GIL
的释放和竞争,这样呈现的结果就是一个线程执行一点字节码,之后另一个线程抢到机会也执行一点字节码,循环往复,直到所有线程执行结束。所以这种机制下,多线程之间是并发的,最终结束的时刻,不会相差太多时间。
python
多线程的机制,如果要深入了解,可以参考笔者以前在Medium Python
系列写的这篇文章,里面已经清楚讲述了python
线程启动的逻辑以及GIL
竞争的逻辑。