GIL线程全局锁的作用是什么
GIL,全称Global Interpreter Lock,这玩意儿在Python圈子里,简直就像是一个不大不小的“黑历史”,每次聊到多线程,绕不开它。今天咱们就来掰扯掰扯,GIL到底是什么?为啥Python写多线程跑不快?怎么破?顺便再带你看看要是面试官真拿这个问你,怎么不露怯还能加分。
GIL是什么妖魔鬼怪?
简单点讲,GIL就是Python解释器给自己上的一把锁,准确来说是CPython(Python官方的那个实现)。干嘛呢?保护解释器内部的内存管理。别看Python代码里变量类型都是对象,这些对象在底层靠引用计数来管理内存,为了保证多个线程同时操作对象时不打架,就整了这么个全局锁。
你可能觉得:“锁就锁呗,不就像Java那种锁对象的锁?”
不一样! Java是锁对象,Python是锁了解释器——整个解释器。这就导致一个CPU核心上,同一时刻只能跑一个线程,其他线程得排队等着。
拿个栗子:
import threading
defworker():
for _ in range(1000000):
pass
threads = []
for _ in range(4):
t = threading.Thread(target=worker)
t.start()
threads.append(t)
for t in threads:
t.join()
看着有4个线程,实际上在一个CPU核心上,它们并没有真正并行,而是串着跑的。这就好比你排了4个人在自动售货机买水,每次只能一个人买,剩下的都得等——这就是GIL。
GIL为什么不影响I/O密集型?
你可能又问了:“那为啥大家说Python多线程搞I/O密集型任务还行?”
嘿,这就是GIL友好的地方。Python解释器在做I/O操作(比如读写文件、网络请求)时会主动释放GIL,这样其他线程就有机会干点别的。比如多线程写个爬虫,还是能跑得飞起的:
import threading
import requests
deffetch(url):
resp = requests.get(url)
print(f'{url}: {resp.status_code}')
urls = ['http://example.com'] * 5
threads = []
for url in urls:
t = threading.Thread(target=fetch, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join()
这时候线程们在等待服务器响应时,GIL就被放了,其他线程趁机干活,I/O任务相互不耽误,效率杠杠的。
CPU密集型任务:GIL的噩梦
但是一旦碰上CPU密集型的任务,比如计算个质数、跑个模型训练,这GIL就成了拦路虎。因为线程们都在死磕CPU,谁也不让谁,GIL一把锁全拦住。
import threading
defcpu_task():
count = 0
for _ in range(10**7):
count += 1
threads = []
for _ in range(4):
t = threading.Thread(target=cpu_task)
t.start()
threads.append(t)
for t in threads:
t.join()
这代码跑完,基本跟单线程一样慢,甚至更慢。为啥?线程切换、资源争夺,上下文切换的成本反而拖了后腿。
那怎么办?多进程!
Python社区早就知道GIL是个坑,所以也给了工具——多进程。每个进程都有自己独立的GIL,相当于每个CPU核心各管一锅,互不干扰。用multiprocessing模块简单上手:
from multiprocessing import Process
defcpu_task():
count = 0
for _ in range(10**7):
count += 1
processes = []
for _ in range(4):
p = Process(target=cpu_task)
p.start()
processes.append(p)
for p in processes:
p.join()
这个就是真正的并行,每个进程各自独立,CPU核都能跑起来,性能杠杠的。但缺点也有:进程间通信复杂,开销大。内存也用得多点,毕竟一个进程一份空间。
协程:单核也能嗨起来
多进程虽然牛,但有点重,协程算是另一种思路。协程不靠系统线程,在单线程里切换任务,像个灵活的调度员,I/O密集场景下一样爽飞。
比如用asyncio搞并发:
import asyncio
import aiohttp
asyncdeffetch(session, url):
asyncwith session.get(url) as resp:
print(f'{url}: {resp.status}')
asyncdefmain():
asyncwith aiohttp.ClientSession() as session:
tasks = [fetch(session, 'http://example.com') for _ in range(5)]
await asyncio.gather(*tasks)
asyncio.run(main())
这种写法,轻巧优雅,尤其适合爬虫、网络应用。但别忘了,协程也只是单核,适合I/O密集,不适合CPU密集型。
面试官问:“Python的GIL你怎么看?”
来,最优解奉上:
Python的GIL是CPython里用来保证解释器内部数据安全的一把全局锁,导致在多线程下,同一时刻只有一个线程能执行字节码。
对于I/O密集型任务,Python多线程通过释放GIL,实现并发效率不错;但对于CPU密集型,多线程反而可能拖慢程序,通常会选择多进程来真正并行,充分利用多核CPU资源。
此外,协程也是一种高效的并发方式,特别适合I/O密集型场景,能大幅降低线程切换的开销。
如果特别在意GIL带来的限制,也可以考虑用不带GIL的Python实现,比如Jython、IronPython,或者直接上C/C++扩展绕过GIL。
——这样答,够技术,够清晰,还带了点思路,面试官绝对点头!