Python-- GIL 锁简述

文章目录

  • 一、GIL 全局解释器锁
  • 二、为什么会有 GIL 锁?
  • 三、GIL 锁与普通锁的区别
    • 3.1 为什么要再加上数据锁?
  • 四、多线程无法利用多核优势?
    • 4.1 代码验证--计算密集型
    • 4.2 代码验证--IO 密集型
  • 五、总结

一、GIL 全局解释器锁

定义:
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.)

翻译结果:

1、GIL 不是 Python 的特点,而是 CPython 解释器的特点;

2、在 CPython 解释器中,GIL 是一把互斥锁,用来阻止同一个进程下多个线程的同时执行

3、因为 CPython 解释器的内存管理并不安全( 内存管理—垃圾回收机制)

稍加解释:在没有 GIL 锁的情况下,有可能多线程在执行一个代码的同时,垃圾回收机制线程对所执行代码的变量直接回收,导致运行报错;

重点:

1、GIL 不是 Python 的特点,而是 CPython 解释器的特点;

2、GIL 锁是加在 CPython 解释器上的,是保证解释器级别的数据的安全;

3、GIL 锁会导致同一个进程下多个线程的不能同时执行

4、不同的数据除了 GIL 锁,还需要一把互斥锁,来保证数据处理不会错乱
注意:GIL 锁是加在 CPython 解释器上的,进程先获取 GIL 锁,在获取 CPython 解释器

二、为什么会有 GIL 锁?

Python 是一门解释型的语言,这就意味着代码是解释一行,运行一行,它并不清楚代码全局;

因此,每个线程在调用 cpython 解释器 在运行之前,需要先抢到 GIL 锁,然后才能运行。

编译型的语言就不会存在 GIL 锁,编译型的语言会直接编译所有代码,就不会出现这种问题。

三、GIL 锁与普通锁的区别

启动 10 个线程 去修改同一个变量–number

方案一:在线程运行的函数中加入 time.sleep(0.1),而且没有加入数据的互斥锁结果为 9 而不是 0,运行时间为 0.1 秒左右

import time
from threading import Thread,Lock


# mutex = Lock()
number = 10

def func():
    global number
    tem = number
    time.sleep(0.1)  
    number = tem -1

if __name__ == '__main__':

    thread_list = []
    for i in range(10):
        thread = Thread(target=func)
        thread.start()
        thread_list.append(thread)
    for i in thread_list:
        i.join()

    print(number)

加上 time.sleep(0.1),会让所有的线程将获得的 GIL 锁运行到这一行代码直接释放掉,其他的线程就能获取到 GIL 锁

运行的步骤为:10 个线程会竞争 GIL 锁,然后运行代码,运行到 time.sleep(0.1),释放 GIL 锁,在等待期间,其他线程获取到 GIL 锁运行代码,再释放GIL 锁… 直到第10 个线程释放 GIL 锁之后,此时10 个进程都已经获取到了 tem = 10。

然后,第一个线程再获取 GIL 锁运行代码,修改 number = 10 - 1 = 9 , 修改结束,第一个线程运行结束,释放 GIL 锁;

其次,第二个线程再获取 GIL 锁运行代码,修改 number = 10 - 1 = 9 , 修改结束,第一个线程运行结束,释放 GIL 锁;

直到全部线程结束,那么结果就是 9 而不是 0

方案二:在线程运行的函数中 删除 time.sleep(0.1),而且没有加入数据的互斥锁结果为 0,运行时间为 0.1 秒左右

import time
from threading import Thread,Lock


# mutex = Lock()
number = 10

def func():
    global number
    tem = number
    # time.sleep(0.1)  
    number = tem -1

if __name__ == '__main__':

    thread_list = []
    for i in range(10):
        thread = Thread(target=func)
        thread.start()
        thread_list.append(thread)
    for i in thread_list:
        i.join()

    print(number)

那么,为什么注释了一行代码,结果却有差距呢?

因为 time.sleep(0.1) 所代表的的操作是 IO 操作,当线程运行到这一行代码的时候,要进行 IO 操作,需要释放 CPU 资源,也就是说,线程这个时候需要等待 IO操作结束,此时线程就会处于"阻塞态"可以简单理解为这个线程什么活都不干了,就等着 IO结束,那么这个线程不干活了,其他线程要干活啊,所以,其他的线程就会获取 GIL 锁,再运行代码。

如果没有 time.sleep(0.1) 所代表的的操作是 IO 操作,线程就会一直运行到结束,才释放 GIL 锁,其他的线程才能获取 GIL 锁,运行代码;

那么,反映在这个例子中,第一个线程获取 GIL 锁以后,会直接运行到最后一步,修改了 number = tem -1=10-1=9;

下一个线程,再获取到的 number 就是 9 了,一次类推,结果为 0;

方案三:在方案一的基础上再加入数据的互斥锁结果为 0, 但是运行时间为 1 秒

import time
from threading import Thread,Lock


mutex = Lock()
number = 10

def func(mutex):
    mutex.acquire()
    global number
    tem = number
    time.sleep(0.1)
    number = tem -1
    mutex.release()

if __name__ == '__main__':

    thread_list = []
    for i in range(10):
        thread = Thread(target=func,args=(mutex,))
        thread.start()
        thread_list.append(thread)
    for i in thread_list:
        i.join()

    print(number)

运行结果的区别在于,第一个线程在运行时,先获取 GIL 锁,然后对 number数据进行上锁,运行到 time.sleep(0.1) 线程变成"阻塞态",释放 GIL 锁;

第二个进程获取 GIL 锁,但是由于 number数据已经被上锁了,无法操作,只能变成"阻塞态",释放 GIL 锁;

其他线程也是一样,当第一个线程 time.sleep(0.1) 运行结束之后(IO 操作结束) ,第一个线程会变成"就绪态",那么就可以获得 GIL 锁,继续运行,直到最终修好 number ,释放掉互斥锁,线程运行结束,然后释放 GIL 锁;

也就是说,当第一个进程运行 time.sleep(0.1) 的时候,其他的线程什么都做不了,只能干等;

当 数据的互斥锁被释放之后,其他的线程就可以获取互斥锁,那么其他的线程都会变成"就绪态",但是只有一个线程能够获取 (服从 CPU 的调度算法,这里不进行拓展了),能够获取互斥锁的线程会对 GIL 锁,互斥锁进行上锁,再运行… …

所以最终的结果是 0,但是运行时间是 每个线程的运行时间相加。

3.1 为什么要再加上数据锁?

就方案二,与方案三而言,明显只有 GIL 锁的运行时间更短,为什么要有方案三呢?

其实,我们在编程时,要考虑到数据传输的延迟问题,很明显,现阶段的计算机的数据传输不可能做到无延迟,那么就必须对数据单独加锁,否则就会出现数据错乱。

四、多线程无法利用多核优势?

由于 GIL 的存在,即使是多个线程处理任务,但是最终也只有一个线程在工作,那么是不是多线程真的一点用处都没有呢?

对于需要执行的任务来说,分为两种:计算密集型、IO 密集型

假如一个 计算密集型 的任务需要 10s 的执行时间,总共有 4 个这样的任务

4核及以上 的情况下:

多进程:需要开启 4 个进程,但是 4 个 CPU 并行,最终只需要消耗 10s 多一点的时间

多线程:只需要开1 个进程,这个进程开启 4 个线程,开启线程所消耗的资源很少,但是由于最终执行是只有一个 CPU 可以工作,所以最终消耗 40s 多的时间

假如是多个 IO密集型 的任务CPU 大多数时间是处于闲置状态,频繁的切换

多进程:进程进行切换需要消耗大量资源

多线程:线程进行切换并不需要消耗大量资源

4.1 代码验证–计算密集型

计算密集型----开启多进程—最终结果为 7.689368247985 s

from multiprocessing import Process
import time
from threading import Thread


def foo():
    res = 1.1
    for i in range(1, 100000000):
        res *= i


if __name__ == '__main__':
    l = []
    start_time = time.time()
    for i in range(8):
        p = Process(target=foo)
        p.start()
        l.append(p)
    for p in l:
        p.join()

    print(time.time() - start_time)

**计算密集型—开启多线程—最终结果为 37.46208214759827 s **

from multiprocessing import Process
import time
from threading import Thread


def foo():
    res = 1.1
    for i in range(1, 100000000):
        res *= i


if __name__ == '__main__':
    l = []
    start_time = time.time()
    for i in range(8):
        p = Thread(target=foo)
        p.start()
        l.append(p)
    for p in l:
        p.join()

    print(time.time() - start_time)

4.2 代码验证–IO 密集型

IO密集型–开启多进程:7.326493978500366

from multiprocessing import Process
import time
from threading import Thread


def foo():
    time.sleep(2)


if __name__ == '__main__':
    l = []
    start_time = time.time()
    for i in range(1000):
        p = Process(target=foo)
        p.start()
        l.append(p)
    for p in l:
        p.join()

    print(time.time() - start_time)

IO密集型–开启多线程:2.4292690753936768

from multiprocessing import Process
import time
from threading import Thread


def foo():
    time.sleep(2)


if __name__ == '__main__':
    l = []
    start_time = time.time()
    for i in range(1000):
        p = Thread(target=foo)
        p.start()
        l.append(p)
    for p in l:
        p.join()

    print(time.time() - start_time)

五、总结

对于多进程、多线程其实都有其各自的应用场景;

对于普通程序猿来说,开发的软件大多是 IO 密集型,所以即使存在 GIL 锁,开启多线程也是有优势的

并且,可以同时开启多进程与多线程,同时兼并二者的优点,至于在何时切换成线程还是进程,则有专门的模块,

感兴趣的读者,可以去查看,搜索。

你可能感兴趣的:(Python基础,mysql)