对GIL锁的理解
【1】介绍
- 在 Python 中,GIL 或全局解释器锁(Global Interpreter Lock)是一个机制,用于限制 Python 解释器在多线程环境中同时执行多个线程的能力。这是 Python 核心解释器(CPython)中的一个重要部分,它的存在主要是为了简化 CPython 在内存管理上的操作,特别是为了避免与垃圾回收机制相关的复杂性。
【2】为什么会有GIL锁
- CPython 使用引用计数作为其主要的内存管理技术,这意味着对象一旦没有引用指向它们时就会被立即清除。如果多个线程能够同时执行 Python 字节码,那么在修改任何对象的引用计数时就可能出现竞争条件。例如,两个线程可能同时增加或减少同一个对象的引用计数,导致引用计数被错误地更新。为了避免这种情况,Python 的设计者引入了 GIL。
- 如果没有这个GIL锁,那么就可能会有一种极限情况,当其中一个线程刚刚申请到内存空间里面的18,还没有来得及绑定给age,这时候垃圾回收机制也执行了,发现居然没有引用计数,直接就把18交给解释器回收掉了。
【3】GIL锁的影响
- GIL的一个重要影响就是他限制了多线程程序在多核处理器上的并行效率,因为有这个GIL,python在同一时刻,一个进程内只能有一个线程运行,这样就无法发挥多核处理器的优势。这也同时意味着在计算密集型任务上,多线程的效率并不高,因为多线程并不能真正的并行。
- 然而,在I/O密集型任务中,多线程依旧是非常有效,因为当一个线程进入IO阻塞时,GIL锁会自动释放,是的其他线程可以正常执行。因此,在 I/O 密集型的应用程序中,多线程可以显著提高程序的总体性能和响应性。
【4】如何应对GIL的限制
- 多进程: 使用
multiprocessing
模块可以创建多个进程,每个进程都有自己的 Python 解释器和内存空间,因此不会受到 GIL 的限制。这对于 CPU 密集型任务非常有效。 - 其他解释器: 考虑使用不受 GIL 限制的 Python 解释器,如 Jython(基于 Java 虚拟机)或 IronPython(基于 .NET 框架)。
- 使用 C 扩展: 编写或使用 C/C++ 扩展可以执行 CPU 密集型任务,因为 C/C++ 代码不受 GIL 的限制。
- 并发库: 使用如
asyncio
(用于异步编程)这样的库可以提高 I/O 密集型任务的效率
【5】为什么有了GIL锁,还需要互斥锁?(面试题)
- 我对此的理解是,大多数时候,我们的传输都是由I/O延迟的,比如说网络延迟就是不可避免的,所以当在传输时,线程遇到了I/O阻塞时就会释放GIL锁,其他线程也是会经历这个过程,所以线程还是可以相对并行的运行,就容易导致读取同一份数据时造成数据错乱。
- 总的来说,GIL 虽然提供了一定程度的线程安全保护,但它主要是为了简化内存管理和避免某些类型的竞争条件。GIL 并不是用于同步线程间对共享数据的访问。因此,当涉及到共享数据和保证线程之间操作顺序的一致性时,互斥锁仍然是必不可少的。
【6】例子
- 这里我用一个简易的模拟买票案例说明
from threading import Thread
tickets = 5
def task(i):
global tickets
if tickets < 1:
print(f'用户{i}购票失败:>>>>余票不足')
else:
tickets -= 1
print(f'用户{i}购票成功')
def task_thread():
t_list = [Thread(target=task, args=(i,)) for i in range(1, 11)]
for t in t_list:
t.start()
if __name__ == '__main__':
task_thread()
'''
用户1购票成功
用户2购票成功
用户3购票成功
用户4购票成功
用户5购票成功
用户6购票失败:>>>>余票不足
用户7购票失败:>>>>余票不足
用户8购票失败:>>>>余票不足
用户9购票失败:>>>>余票不足
用户10购票失败:>>>>余票不足
'''
- 可以看到在这段代码中我并没有加上互斥锁,但是多个线程之间几乎是同时访问
tickets
这个数据,但是输出的结果却满足我的要求,当票没了,后面的顾客就无法在购票,并没有造成数据错乱。 - 这就印证了GIL锁的存在,当然这是在理想情况下,也就是没有IO阻塞的情况下。
- 当我给程序加上那么一点的阻塞,再看看效果
from threading import Thread
import time
tickets = 5
def task(i):
global tickets
if tickets < 1:
time.sleep(0.05)
print(f'用户{i}购票失败:>>>>余票不足')
else:
time.sleep(0.05)
tickets -= 1
print(f'用户{i}购票成功')
def task_thread():
t_list = [Thread(target=task, args=(i,)) for i in range(1, 11)]
for t in t_list:
t.start()
if __name__ == '__main__':
task_thread()
'''
用户6购票成功
用户3购票成功
用户2购票成功
用户7购票成功
用户4购票成功
用户1购票成功
用户5购票成功
用户9购票成功
用户10购票成功
用户8购票成功
进程已结束,退出代码0
'''
- 可以看到,当我在买票环节上加上了0.05的延迟,输出的结果就达不到我的预期了,明明只有5张票,却10个人都买到了票
- 说明GIL在遇到IO阻塞时就会自动释放锁,这时候就需要我们自己加上互斥锁以保证程序的正常运行了。
from threading import Thread, Lock
import time
tickets = 5
mutex = Lock()
def task(i):
global tickets
mutex.acquire()
if tickets < 1:
time.sleep(0.05)
print(f'用户{i}购票失败:>>>>余票不足')
else:
time.sleep(0.05)
tickets -= 1
print(f'用户{i}购票成功')
mutex.release()
def task_thread():
t_list = [Thread(target=task, args=(i,)) for i in range(1, 11)]
for t in t_list:
t.start()
if __name__ == '__main__':
task_thread()
'''
用户1购票成功
用户2购票成功
用户3购票成功
用户4购票成功
用户5购票成功
用户6购票失败:>>>>余票不足
用户7购票失败:>>>>余票不足
用户8购票失败:>>>>余票不足
用户9购票失败:>>>>余票不足
用户10购票失败:>>>>余票不足
进程已结束,退出代码0
'''
- 所以我认为在后续的编程中,该加锁还得加锁