乐趣区

关于python:Python-多线程居然是-假的

不过最近有位读者发问:

Python 的多线程真是假的吗?

一下子点到了 Python 长期被人们喜忧参半的个性 —— GIL 上了。

到底是怎么回事呢?明天咱们来聊一聊。

美中不足

咱们晓得 Python 之所以灵便和弱小,是因为它是一个解释性语言,边解释边执行,实现这种个性的规范实现叫作 CPython。

它分两步来运行 Python 程序:

  • 首先解析源代码文本,并将其编译为字节码(bytecode)[1]
  • 而后采纳基于栈的解释器来运行字节码
  • 一直循环这个过程,直到程序完结或者被终止

灵活性有了,然而为了保障程序执行的稳定性,也付出了微小的代价:

引入了 全局解释器锁 GIL(global interpreter lock)[2]

以保障同一时间只有一个字节码在运行,这样就不会因为没用当时编译,而引发资源抢夺和状态凌乱的问题了。

看似“美中不足”,但,这样做,就意味着多线程执行时,会被 GIL 变为单线程,无奈充分利用硬件资源。

来看代码:

import time

def gcd(pair):
    '''求解最大公约数'''
    a, b = pair
    low = min(a, b)
    for i in range(low, 0, -1):
        if a % i == 0 and b % i == 0:
            return i
    
    assert False, "Not reachable"

# 待求解的数据
NUMBERS = [(1963309, 2265973), (5948475, 2734765),
    (1876435, 4765849), (7654637, 3458496),
    (1823712, 1924928), (2387454, 5873948),
    (1239876, 2987473), (3487248, 2098437),
    (1963309, 2265973), (5948475, 2734765),
    (1876435, 4765849), (7654637, 3458496),
    (1823712, 1924928), (2387454, 5873948),
    (1239876, 2987473), (3487248, 2098437),
    (3498747, 4563758), (1298737, 2129874)
]

## 程序求解
start = time.time()
results = list(map(gcd, NUMBERS))
end = time.time()
delta = end - start
print(f'程序执行工夫: {delta:.3f} 秒')
  • 函数 gcd 用于求解最大公约数,用来模仿一个数据操作
  • NUMBERS 为待求解的数据
  • 求解形式利用 map 办法,传入处理函数 gcd, 和待求解数据,将返回一个后果数列,最初转化为 list
  • 将执行过程的耗时计算并打印进去

在笔者的电脑上(4 核,16G)执行工夫为 2.043 秒。

如何换成多线程呢?

...

from concurrent.futures import ThreadPoolExecutor

...

## 多线程求解
start = time.time()
pool = ThreadPoolExecutor(max_workers=4)
results = list(pool.map(gcd, NUMBERS))
end = time.time()
delta = end - start
print(f'执行工夫: {delta:.3f} 秒')
  • 这里引入了 concurrent.futures 模块中的线程池,用线程池实现起来比拟不便
  • 设置线程池为 4,次要是为了和 CPU 的核数匹配
  • 线程池 pool 提供了多线程版的 map,所以参数不变

看看运行成果:

程序执行工夫: 2.045 秒

并发执行工夫: 2.070 秒

what?

并行执行的工夫居然更长了!

间断执行屡次,后果都是一样的,也就是说在 GIL 的限度下,多线程是有效的,而且因为线程调度还多损耗了些工夫。

戴着镣铐跳舞

难道 Python 里的多线程真的没用吗?

其实也并不是,尽管了因为 GIL,无奈实现真正意义上的多线程,但,多线程机制,还是为咱们提供了两个重要的个性。

一:多线程写法能够让某些程序更好写

怎么了解呢?

如果要解决一个须要同时保护多种状态的程序,用单线程是实现是很艰难的。

比方要检索一个文本文件中的数据,为了进步检索效率,能够将文件分成小段的来解决,最先在那段中找到了,就完结处理过程。

用单线程的话,很难实现同时兼顾多个分段的状况,只能程序,或者用二分法执行检索工作。

而采纳多线程,能够将每个分段交给每个线程,会轮流执行,相当于同时举荐检索工作,解决起来,效率会比程序查找大大提高。

二:解决阻塞型 I/O 工作效率更高

阻塞型 I/O 的意思是,当零碎须要与文件系统(也包含网络和终端显示)交互时,因为文件系统相比于 CPU 的处理速度慢得多,所以程序会被设置为阻塞状态,即,不再被调配计算资源。

直到文件系统的后果返回,才会被激活,将有机会再次被调配计算资源。

也就是说,处于阻塞状态的程序,会始终等着。

那么如果一个程序是须要一直地从文件系统读取数据,解决后在写入,单线程的话就须要等等读取后,能力解决,期待解决完能力写入,于是处理过程就成了一个个的期待。

而用多线程,当一个处理过程被阻塞之后,就会立刻被 GIL 切走,将计算资源分配给其余能够执行的过程,从而提醒执行效率。

有了这两个个性,就阐明 Python 的多线程并非一无是处,如果能依据状况编写好,效率会大大提高,只不过对于计算密集型的工作,多线程个性心有余而力不足。

曲线救国

那么有没有方法,真正的利用计算资源,而不受 GIL 的解放呢?

当然有,而且还不止一个。

先介绍一个简略易用的形式。

回顾下后面的计算最大公约数的程序,咱们用了线程池来解决,不过没用成果,而且比不必更蹩脚。

这是因为这个程序是计算密集型的,次要依赖于 CPU,显然会受到 GIL 的束缚。

当初咱们将程序稍作批改:

...
from concurrent.futures import ProcessPoolExecutor

...

## 并行程求解
start = time.time()
pool = ProcessPoolExecutor(max_workers=4)
results = list(pool.map(gcd, NUMBERS))
end = time.time()
delta = end - start
print(f'并行执行工夫: {delta:.3f} 秒')

看看成果:

程序执行工夫: 2.018 秒

并发执行工夫: 2.032 秒

并行执行工夫: 0.789 秒

并行执行晋升了将近 3 倍!什么状况?

认真看下,次要是将多线程中的 ThreadPoolExecutor 换成了 ProcessPoolExecutor,即过程池执行器。

在同一个过程里的 Python 程序,会受到 GIL 的限度,但不同的过程之间就不会了,因为每个过程中的 GIL 是独立的。

是不是很神奇?这里,多亏了 concurrent.futures 模块将实现过程池的复杂度封装起来了,留给咱们简洁优雅的接口。

这里须要留神的是,ProcessPoolExecutor 并非万能的,它比拟适宜于 数据关联性低 ,且是  计算密集型 的场景。

如果数据关联性强,就会呈现过程间“通信”的状况,可能使好不容易换来的性能晋升化为泡影。

解决过程池,还有什么办法呢?那就是:

用 C 语言重写一遍须要晋升性能的局部

不要惊愕,Python 里曾经留好了针对 C 扩大的 API。

但这样做须要付出更多的代价,为此还能够借助于 SWIG[3] 以及 CLIF[4] 等工具,将 python 代码转为 C。

有趣味的读者能够钻研一下。

自暴自弃

理解到 Python 多线程的问题和解决方案,对于钟爱 Python 的咱们,何去何从呢?

有句话用在这里很适合:

求人不如求己

哪怕再怎么厉害的工具或者武器,都无奈解决所有的问题,而问题之所以能被解决,次要是因为咱们的主观能动性。

对状况进行分析判断,抉择适合的解决方案,不就是须要咱们做的么?

对于 Python 中 多线程的诟病,咱们更多的是看到它阳光和美的一面,而对于须要晋升速度的中央,采取适合的形式。这里简略总结一下:

  1. I/O 密集型的工作,采纳 Python 的多线程齐全没用问题,能够大幅度提高执行效率
  2. 对于计算密集型工作,要看数据依赖性是否低,如果低,采纳 ProcessPoolExecutor 代替多线程解决,能够充分利用硬件资源
  3. 如果数据依赖性高,能够思考将要害的中央该用 C 来实现,一方面 C 自身比 Python 更快,另一方面,C 能够之间应用更底层的多线程机制,而齐全不必放心受 GIL 的影响
  4. 大部分状况下,对于只能用多线程解决的工作,不必太多思考,之间利用 Python 的多线程机制就好了,不必思考太多

总结

没用美中不足的解决方案,如果有,也只能是在某个具体的条件之下,就像软件工程中,没用银弹一样。

面对实在的世界,只有咱们本人是能够依附的,咱们通过学习理解更多,通过实际,感触更多,通过总结复盘,播种更多,通过思考反思,解决更多。这就是咱们人类一直倒退前行的原动力。

为了咱们美妙的今天,为了人类美妙的今天,加油!

以上就是本次分享的所有内容,如果你感觉文章还不错,欢送关注公众号:Python 编程学习圈,每日干货分享,发送“J”还可支付大量学习材料。或是返回编程学习网,理解更多编程技术常识。

退出移动版