GIL全局解释器锁
(1)简介
-
在 CPython 中,GIL(全局解释器锁)是一种机制,用于确保在同一时刻只有一个线程执行 Python 字节码。这个锁对于 Python 解释器来说是必要的,因为 CPython 的内存管理并不是线程安全的。当多个线程试图执行 Python 代码时,GIL 会确保同一时刻只有一个线程能够执行字节码指令,这样可以避免多线程并发执行时的数据竞争和内存一致性问题。
-
简单来说,GIL 会在解释器级别上进行加锁,这意味着在同一时刻只有一个线程能够执行 Python 代码。即使在多核处理器上,由于 GIL 的存在,Python 解释器仍然只能够利用单个核心执行 Python 代码,因为即使有多个线程,它们也无法同时执行 Python 字节码指令。
-
虽然 GIL 确保了线程安全,但它也带来了一些缺点。其中之一是它限制了 Python 在多核处理器上的并行性能,因为无法充分利用多个 CPU 核心。这也意味着在 CPU 密集型的任务中,Python 程序的性能可能受到影响。然而,对于 I/O 密集型任务,GIL 的影响通常较小,因为线程在等待 I/O 操作完成时会释放 GIL,允许其他线程执行。
from threading import Thread,Lock
import time
money = 100
lock = Lock()
def task():
global money
with lock:
temp = money
time.sleep(2)
money = temp - 1
def main():
t_list = []
for i in range(5):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
# 所有子线程结束后打印 money
print(money)
if __name__ == '__main__':
main()
# 95
(2)GIL导致多线程无法利用多核优势
-
在
Cpython
解释器中GIL
是一把互斥锁,用来阻止同一个进程下的多个线程的同时进行-
同一个进程下的多个线程无法利用这一优势?
-
Python的多线程是不是一点用都没有?
-
-
在
Cpython
中的内存管理不是线程安全的- ps:内存管理(垃圾回收机制)
- 应用计数
- 标记清除
- 分代回收
- ps:内存管理(垃圾回收机制)
(1)Python的多线程是不是一点用都没有?
- 同一个进程下的多线程无法利用多核优势,是不是就没用了
- 多线程是否有用要看情况
- 单核
- 四个任务(IO密集型/计算密集型)
- 多核
- 四个任务(IO密集型/计算密集型)
- 单核
(1)计算密集型
- 每个任务都需要 10s
- 单核
- 多进程:额外消耗资源
- 多线程:减少开销
- 多核
- 多进程:总耗时 10s+
- 多线程:总耗时 40s+
- 单核
from multiprocessing import Process
from threading import Thread
import time, os
def work():
res = 0
for i in range(1, 100000000):
res *= i
def main_t():
p_list = []
# 获取当前CPU运行的个数
print(os.cpu_count())
start_time = time.time()
for i in range(12):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print(f'总耗时:>>>{time.time() - start_time}')
def main_p():
t_list = []
# 获取当前CPU运行的个数
print(os.cpu_count())
start_time = time.time()
for i in range(12):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(f'总耗时:>>>{time.time() - start_time}')
if __name__ == '__main__':
main_t() # 总耗时:>>>13.233189344406128
main_p() # 总耗时:>>>45.123342752456665
(2)IO密集型
- 每个任务都需要 10s
- 多核
- 多进程:相对浪费资源
- 多线程:更加节省资源
- 多核
from multiprocessing import Process
from threading import Thread
import time, os
def work():
time.sleep(2)
def main_t():
p_list = []
# 获取当前CPU运行的个数
print(os.cpu_count())
start_time = time.time()
for i in range(400):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print(f'总耗时:>>>{time.time() - start_time}')
def main_p():
t_list = []
# 获取当前CPU运行的个数
print(os.cpu_count())
start_time = time.time()
for i in range(400):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(f'总耗时:>>>{time.time() - start_time}')
if __name__ == '__main__':
main_t() # 总耗时:>>>33.62358331680298
main_p() # 总耗时:>>>2.073925733566284
(3)总结
(1)计算密集型任务(多进程)
- 计算密集型任务主要是指需要大量的CPU计算资源的任务,其中包括执行代码、进行算术运算、循环等。
- 在这种情况下,使用多线程并没有太大的优势。
- 由于Python具有全局解释器锁(Global Interpreter Lock,GIL),在同一时刻只能有一条线程执行代码,这意味着在多线程的情况下,同一时刻只有一个线程在执行计算密集型任务。
- 但是,如果使用多进程,则可以充分利用多核CPU的优势。
- 每个进程都有自己独立的GIL锁,因此多个进程可以同时执行计算密集型任务,充分发挥多核CPU的能力。
- 通过开启多个进程,我们可以将计算密集的任务分配给每个进程,让每个进程都独自执行任务,从而提高整体的计算效率。
(2)IO密集型任务(多线程)
- IO密集型任务主要是指涉及大量输入输出操作(如打开文件、写入文件、网络操作等)的任务。
- 在这种情况下,线程往往会因为等待IO操作而释放CPU执行权限,不会造成太多的CPU资源浪费。
- 因此,使用多线程能够更好地处理IO密集型任务,避免了频繁切换进程的开销。
- 当我们在一个进程中开启多个IO密集型线程时,大部分线程都处于等待状态,开启多个进程却不能提高效率,反而会消耗更多的系统资源。
- 因此,在IO密集型任务中,使用多线程即可满足需求,无需开启多个进程。
(3)总结
- 计算密集型任务使用多进程可以充分利用多核CPU的优势,而IO密集型任务使用多线程能够更好地处理IO操作,避免频繁的进程切换开销。
- 根据任务的特性选择合适的并发方式可以有效提高任务的执行效率。