GIL全局解释器锁

GIL全局解释器锁介绍
【1】官方解释
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly

because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)

结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
【2】Python解释器版本
● Cpython
● Jpython
● Pypypython
● 但是普遍使用的都是Cpython解释器
【二】GIL锁与普通互斥锁的区别
【1】普通版 1.0
● 当睡了 0.1s 后
● 所有线程都去抢那把 GIL 锁住的数据,当所有子线程都抢到后再去修改数据就变成了 99
from threading import Thread
import time

money = 100

def work():
global money
temp = money
time.sleep(0.1)
money = temp - 1

def main():
task_list = [Thread(target=work) for i in range(100)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(money)

if name == 'main':
main()
# 99
【2】升级版 2.0
● 谁先抢到谁就先处理数据
from threading import Thread
import time

money = 100

def work():
global money

temp = money
money = temp -1

def main():
task_list = [Thread(target=work) for i in range(100)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(money)

if name == 'main':
main()
# 0
【3】终极版 3.0
● 自动加锁并解锁
● 子线程启动 , 后先去抢 GIL 锁 , 进入 IO 自动释放 GIL 锁 , 但是自己加的锁还没解开 ,其他线程资源能抢到 GIL 锁,但是抢不到互斥锁
● 最终 GIL 回到 互斥锁的那个进程上,处理数据
from threading import Thread, Lock
import time

mutex = Lock()

money = 100

def work():
global money

# 自动执行 加锁 再解锁操作
with mutex:
    temp = money
    # 只要进入 IO 会自动释放 GIL 锁
    time.sleep(0.1)
    money = temp -1

def main():
task_list = [Thread(target=work) for i in range(100)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(money)

if name == 'main':
main()
# 0
【三】GIL导致多线程无法利用多核优势
【1】 Cpython 解释器中 GIL
● 在 Cpython 解释器中 GIL 是一把互斥锁,用来阻止同一个进程下的多个线程的同时进行
○ 同一个进程下的多个线程无法利用这一优势?
○ Python的多线程是不是一点用都没有?
● 因为在 Cpython 中的内存管理不是线程安全的
○ ps:内存管理(垃圾回收机制)
■ 应用计数
■ 标记清除
■ 分代回收
【2】Python的多线程是不是一点用都没有?
● 同一个进程下的多线程无法利用多核优势,是不是就没用了
● 多线程是否有用要看情况
○ 单核
■ 四个任务(IO密集型/计算密集型)
○ 多核
■ 四个任务(IO密集型/计算密集型)
(1)计算密集型
一直处在计算运行中
● 每个任务都需要 10s
○ 单核
■ 多进程:额外消耗资源
■ 多线程:减少开销
○ 多核
■ 多进程:总耗时 10s
■ 多线程:总耗时 40s+
from multiprocessing import Process
from threading import Thread
import time
import os

def work_calculate(n=1000):
result = 1
for i in range(1, n + 1):
result *= i
# 为了确保计算确实发生并防止编译器优化,可以考虑使用结果
_ = sum(int(digit) for digit in str(result))

def timer(func):
def inner(args, **kwargs):
# 定义起始时间
start_time = time.time()
func(
args, **kwargs)
# 计算总耗时
print(f'总耗时:>>>{time.time() - start_time}')

return inner

@timer
def main(cls):
# 获取当前CPU运行的个数
print(f'当前正在使用的CPU个数 :>>>> {os.cpu_count()}')

# 创建任务列表
task_list = [cls(target=work_calculate) for i in range(400)]
# 启动任务
[task.start() for task in task_list]
# 阻塞任务
[task.join() for task in task_list]

if name == 'main':
# 计算密集型
# 多进程下的耗时
# main(cls=Process)
# 当前正在使用的CPU个数 :>>>> 12
# 总耗时:>>>15.169148921966553

# 多线程下的耗时
main(cls=Thread)
# 当前正在使用的CPU个数 :>>>> 12
# 总耗时:>>>5.78026819229126

(2)IO密集型
存在多个 IO 阻塞切换操作
● 每个任务都需要 10s
○ 多核
■ 多进程:相对浪费资源
■ 多线程:更加节省资源
from multiprocessing import Process
from threading import Thread
import time
import os

def work_io():
time.sleep(2)

def work_calculate(n=1000):
result = 1
for i in range(1, n + 1):
result *= i
# 为了确保计算确实发生并防止编译器优化,可以考虑使用结果
_ = sum(int(digit) for digit in str(result))

def timer(func):
def inner(args, **kwargs):
# 定义起始时间
start_time = time.time()
func(
args, **kwargs)
# 计算总耗时
print(f'总耗时:>>>{time.time() - start_time}')

return inner

@timer
def main(cls):
# 获取当前CPU运行的个数
print(f'当前正在使用的CPU个数 :>>>> {os.cpu_count()}')

# 创建任务列表
task_list = [cls(target=work_io) for i in range(400)]
# 启动任务
[task.start() for task in task_list]
# 阻塞任务
[task.join() for task in task_list]

if name == 'main':
# 计算密集型
# 多进程下的耗时
main(cls=Process)
# 当前正在使用的CPU个数 :>>>> 12
# 计算密集型 总耗时:>>>14.954952001571655
# IO密集型 总耗时:>>>17.02257513999939

# 多线程下的耗时
# main(cls=Thread)
# 当前正在使用的CPU个数 :>>>> 12
# 计算密集型 总耗时:>>>0.2667570114135742
# IO密集型 总耗时:>>>2.039600133895874

【3】小结
● 计算是消耗cpu的:
○ 代码执行,算术,for都是计算
● io不消耗cpu:
○ 打开文件,写入文件,网络操作都是io
○ 如果遇到io,该线程会释放cpu的执行权限,cpu转而去执行别的线程
● 由于python有gil锁,开启多条线程,统一时刻,只能有一条线程在执行
● 如果是计算密集型,开了多线程,同一时刻,只有一个线程在执行
● 多核cpu,就会浪费多核优势
● 如果是计算密集型,我们希望,多个核(cpu),都干活,同一个进程下绕不过gil锁
● 所以我们开启多进程,gil锁只能锁住某个进程中得线程,开启多个进程,就能利用多核优势
● io密集型---》只要遇到io,就会释放cpu执行权限
● 进程内开了多个io线程,线程多半都在等待,开启多进程是不能提高效率的,反而开启进程很耗费资源,所以使用多线程即可
(1)计算密集型任务(多进程)
● 计算密集型任务主要是指需要大量的CPU计算资源的任务,其中包括执行代码、进行算术运算、循环等。
○ 在这种情况下,使用多线程并没有太大的优势。
○ 由于Python具有全局解释器锁(Global Interpreter Lock,GIL),在同一时刻只能有一条线程执行代码,这意味着在多线程的情况下,同一时刻只有一个线程在执行计算密集型任务。
● 但是,如果使用多进程,则可以充分利用多核CPU的优势。
○ 每个进程都有自己独立的GIL锁,因此多个进程可以同时执行计算密集型任务,充分发挥多核CPU的能力。
○ 通过开启多个进程,我们可以将计算密集的任务分配给每个进程,让每个进程都独自执行任务,从而提高整体的计算效率。
(2)IO密集型任务(多线程)
● IO密集型任务主要是指涉及大量输入输出操作(如打开文件、写入文件、网络操作等)的任务。
○ 在这种情况下,线程往往会因为等待IO操作而释放CPU执行权限,不会造成太多的CPU资源浪费。
○ 因此,使用多线程能够更好地处理IO密集型任务,避免了频繁切换进程的开销。
● 当我们在一个进程中开启多个IO密集型线程时,大部分线程都处于等待状态,开启多个进程却不能提高效率,反而会消耗更多的系统资源。
○ 因此,在IO密集型任务中,使用多线程即可满足需求,无需开启多个进程。
(3)总结
● 计算密集型任务使用多进程可以充分利用多核CPU的优势,而IO密集型任务使用多线程能够更好地处理IO操作,避免频繁的进程切换开销。
○ 根据任务的特性选择合适的并发方式可以有效提高任务的执行效率。
【四】GIL特点总结
● 1.GIL 不是python的特点而是Cpython解释器的特点
● 2.GIL 保证解释器级别的数据的安全
● 3.GIL会导致同一个进程下的多个线程的无法同时进行即无法利用多核优势
● 4.针对不同的数据还是需要加不同的锁处理
● 5.解释型语言的通病:同一个进程下的多个线程无法利用多核优势

posted @ 2024-05-23 13:25  zenopan  阅读(14)  评论(0编辑  收藏  举报