100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > c语言的锁和Python锁 Python中全局解释器锁 多线程和多进程

c语言的锁和Python锁 Python中全局解释器锁 多线程和多进程

时间:2021-03-02 04:16:30

相关推荐

c语言的锁和Python锁 Python中全局解释器锁 多线程和多进程

全局解释器锁(GIL)只允许1个Python线程控制Python解释器。这也就意味着同一时间点只能有1个Python线程运行。如果你的Python程序只有一个线程,那么全局解释器锁可能对你的影响不大,但是如果你的程序是CPU密集型同时使用了多线程,那么程序运行可能会受到很大影响。全局解释器锁在Python中饱受诟病,但是它确实为Python内存处理提供了方便。

什么是全局解释器锁

Python通过引用计数的方法来管理内存,每一个内存对象都会有一个引用计数,如果一个对象的引用计数变为0,那么该对象所占用的内存就会被释放。比如

import sys

a = []

b = a

sys.getrefcount(a)

## 3

在上述代码中,空列表对象在内存中被引用了3次,即分别被a和b引用,同时被sys.getrefcount()调用。

但是,如果一个对象同时被多个线程来引用,那么引用计数可能同时增加或减少,每个线程按照自己的方式进行计数,对象在整个内存中的引用变得十分混乱,很容易造成内存泄漏或者其他很多不可预见的Bug。一个解决办法给就是每个线程都给引用计数加一个锁,阻止别人修改,不过这样会造成锁死现象(比如一个对象有多个锁时),另外,大量的资源会浪费在加锁解锁的过程了,严重拖慢了程序运行速度。

这个时候,就需要一个统一来管理引用计数的机制,以确保对象引用计数准确、安全。全局解释器锁就是用来处理这种情况的。它既避免了不同线程带来的引用计数混乱,又避免了过多线程锁带来的死锁和运行效率低的问题。

虽然全局解释器锁解决了对象引用计数的问题,但随之而来的是,很多CPU密集型任务在全局解释器锁的作用下,实际上变成了单线程,不能充分发挥CPU的算力,影响程序速度。

全局解释器锁并不是Python独有的,其他一些语言,比如Ruby也存在全局解释器锁。还有一些语言没有使用引用计数的方式来管理内容,而是使用垃圾回收机制(GC)来管理内存。虽然这样避免了全局解释器锁,但是在单线程处理上,GC并不占有优势。

为什么Python选择全局解释器锁

Python在设计之初,就选择了全局解释器锁用来管理内存引用计数,在当时,操作系统还没有线程的概念,所以全局解释器锁并没有带了弊端,反而给开发者带来了很多方便。

很多Python的扩展都是使用C语言库来编写的,这些C编写的扩展需要全局解释器锁来确保线程的内存安全。即便是有些C语言库内存处理上不是很安全,那么在Python中全局解释器锁的作用下,也能很好的发挥作用。

所以,全局解释器锁对早期使用CPython做解释器的开发者来说,解决了很多内存管理的问题。

全局解释器锁对多线程的影响

首先我们应该区分不同性质的任务,有一些是CPU密集型的任务,有一些是I/O密集型的任务。

CPU密集型的任务在最大程度上使用了CPU,比如数学矩阵的计算、图像处理、文件解压缩等。

I/O密集型任务在需要花费很多时间来等待信息的输入和输出(读写),比如从网址下载内容,大量访问磁盘进行读写等。

如果是CPU密集型任务,比如下面我们进行倒计时,使用单线程:

# 单线程

import time

from threading import Thread

COUNT = 50000000

def countdown(n):

while n>0:

n -= 1

start = time.time()

countdown(COUNT)

end = time.time()

print('Time taken in seconds -', end - start)

# Time taken in seconds - 6.20024037361145

最后倒计时完成,一共用了6.2秒

使用多线程

# 使用多线程

import time

from threading import Thread

COUNT = 50000000

def countdown(n):

while n>0:

n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))

t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()

t1.start()

t2.start()

t1.join()

t2.join()

end = time.time()

print('Time taken in seconds -', end - start)

# Time taken in seconds - 6.924342632293701

最后用了6.9秒!

可以看出来,对于这个CPU密集型的任务,多线程使用的时间竟然比单线程还多,使用多线程并没有达到我们缩短运行时间的目的。这就是全局解释器锁带来的问题。但是如果是I/O密集型的任务,全局解释器锁就不会再有这种影响,各个线程在等待I/O的时候可以共享锁。

为什么全局解释器锁还保留在Python中

首先全局解释器锁一直在Python中使用,如果在某个版本中移除了全局解释器锁,会带来各种兼容性问题,尤其是对于Python这种有大量开发者的编程语言来说,稍微一点变动都会带来巨大的影响。另外,很多C语言编写的扩展严重依赖于局解释器锁。况且,如果移除了全局解释器锁,那么又可能会造成单线程或者I/O多线程处理速度的下降。

当Python从2.0升级到3.0的大版本升级时,实际上是有机遇改变全局解释器锁的。但是这使得Python3在处理单线程或者I/O多线程时,比Python2还慢!所以Python3中依旧保留了全局解释器锁。当然,在Python3中,对全局解释器锁进行了修改和优化,即减少I/O所线程使用全局解释器锁的机会,把更多的时间和机会留给CPU密集型多线程任务。另外,如果一个线程持续使用全局解释器锁一段时间后,程序会强制其释放全局解释器锁,当然如果没有其他线程请求使用全局解释器锁,那么该线程还可以继续使用全局解释器锁。

我们应该应对全局解释器锁

既然全局解释器锁带来一些问题,那么我们怎么来解决这些问题?这儿我们要说两个不同的概念:

多进程(multi-processing) 和多线程(multi-threading):

多进程是各个并行任务之间“不使用“共同的内存空间;而多线程的各个并行任务”使用“共同的内存空间。

1)多进程

优点:独立内存空间;实现代码直观简单;充分利用多核多CPU;避免全局解释器锁的限制;

缺点:无法实现对象和内容共享;需要较大的内存空间

2)多线程

优点:轻量,需要的额外内存较小;共享内存,方便访问;对于CPython解释器,可以通过全局解释器锁使用C扩展;适合I/O密集型任务

缺点:全局解释器锁的限制;并行任务不能杀掉;实现代码较为复杂

我们通过多进程的形式来实现上面例子中的倒计时,

from multiprocessing import Pool

import time

COUNT = 50000000

def countdown(n):

while n>0:

n -= 1

if __name__ == '__main__':

pool = Pool(processes=2)

start = time.time()

r1 = pool.apply_async(countdown, [COUNT//2])

r2 = pool.apply_async(countdown, [COUNT//2])

pool.close()

pool.join()

end = time.time()

print('Time taken in seconds -', end - start)

# Time taken in seconds - 4.060242414474487

通过多进程只用了4秒,速度大大加快了。

另外,全局解释器锁存在于Cpython的解释器中,如果你使用其他的Python解释器,比如Jython(使用Java编写), IronPython(使用C#编写)和PyPy(使用Python编写),可能不会遇到全局解释器锁的情况。

总结

全局解释器锁在CPython中,限制了CPU密集型的多线程处理任务。遇到CPU密集型的多线程处理任务时,我们尽量使用多进程的方式来处理(multiprocessing)。

=====》》》《《《《======

扩展:在R、Python和shell中如何实现多进程处理:

资料来源:

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。