GIL全局解释器锁

GIL全局解释器锁

  前提

    首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

  GIL介绍    

    在Cpython中GIL全局解释器锁其实也是一把互斥锁,主要用于阻止同一个进程下的多个线程同时被运行(python的多线程无法使用多核优势)。
    GIL肯定存在于CPython解释器中 主要原因就在于Cpython解释器的内存管理不是线程安全的。

    在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问。

    所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。

    如果多个线程的target=work,那么执行流程是:

    多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行

    解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码。

 

 

 

   特点总结

    1.GIL是Cpython解释器的特点
    2.python同一个进程内的多个线程无法利用多核优势(不能并行但是可以并发)
    3.同一个进程内的多个线程要想运行必须先抢GIL锁
    4.所有的解释型语言几乎都无法实现同一个进程下的多个线程同时被运行

验证GIL的存在

  

复制代码
from threading import Thread
import time

m = 100


def test():
    global m
    tmp = m
    tmp -= 1
    m = tmp


for i in range(100):
    t = Thread(target=test)
    t.start()
time.sleep(3)
print(m)

# 即使不加互斥锁结果依然为0,可见GIL是存在的
复制代码

 

  注意:

    同一个进程下的多个线程虽然有GIL的存在不会出现并行的效果
    但是如果线程内有IO操作还是会造成数据的错乱 这个时候需要我们额外的添加互斥锁

死锁现象

  所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁:

复制代码
from threading import Thread, Lock
import time

A = Lock()
B = Lock()


class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        A.acquire()
        print('%s 抢到了A锁' % self.name)  # current_thread().name  获取线程名称
        B.acquire()
        print('%s 抢到了B锁' % self.name)
        time.sleep(1)
        B.release()
        print('%s 释放了B锁' % self.name)
        A.release()
        print('%s 释放了A锁' % self.name)

    def func2(self):
        B.acquire()
        print('%s 抢到了B锁' % self.name)
        A.acquire()
        print('%s 抢到了A锁' % self.name)
        A.release()
        print('%s 释放了A锁' % self.name)
        B.release()
        print('%s 释放了B锁' % self.name)


for i in range(10):
    obj = MyThread()
    obj.start()

'''
结果为:
Thread-1 抢到了A锁
Thread-1 抢到了B锁
Thread-1 释放了B锁
Thread-1 释放了A锁
Thread-2 抢到了A锁
Thread-1 抢到了B锁

'''
死锁现象
复制代码

  因此就算知道锁的特性及使用方式 也不要轻易的使用 因为容易产生死锁现象。

python多线程性能对比

  IO密集型

    假如有四个任务,每个任务耗时10s

    开设多进程遇到IO就需要切换 并且开设进程还需要申请内存空间和拷贝代码,预计时间为10s++,因此没有多大优势。

    开设多线程不需要消耗额外的资源,预计时间10s+,有优势。

复制代码
from multiprocessing import Process
from threading import Thread
import threading
import os, time


def work():
    time.sleep(2)


if __name__ == '__main__':
    l = []
    print(os.cpu_count())  # 本机为4核
    start = time.time()
    for i in range(400):
        p = Process(target=work)  # 耗时21.52s多,大部分时间耗费在创建进程上
        # p = Thread(target=work)  # 耗时2.18s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop = time.time()
    print('run time is %s' % (stop - start))
复制代码

 

  计算密集型

    假如有四个任务,每个任务耗时10s

    开设多进程可以利用多核优势,预计完成时间10s+

    开设多线程无法利用多核优势  ,预计完成时间40s+

复制代码
from multiprocessing import Process
import os, time


def work():
    res = 0
    for i in range(100000000):
        res *= i


if __name__ == '__main__':
    l = []
    print(os.cpu_count())  # 本机为4核
    start = time.time()
    for i in range(4):
        p = Process(target=work)  # 耗时6.57s多
        # p = Thread(target=work)  # 耗时21.13s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop = time.time()
    print('run time is %s' % (stop - start))
复制代码

 

posted @   临江沂水  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示