~~并发编程(十一):GIL全局解释锁~~

进击のpython

*****

并发编程——GIL全局解释锁


这小节就是有些“大神”批判python语言不完美之处的开始

这一节我们要了解一下Cpython的GIL解释器锁的工作机制

掌握一下GIL和互斥锁

最后再了解一下Cpython下多线程和多进程各自的应用场景

首先需要明确的一点就是GIL不是Python的特性

他是实现Python解释器(Cpython)时所引入的一个概念

当然Python不止这一个解释器来编译代码

只是因为Cpython是大部分默认环境下的Python执行环境

所以在很多人的概念里CPython就是Python

也就想当然的把GIL归结为Python语言的缺陷

所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL


GIL介绍

其实GIL本质上就是一把互斥锁,既然是互斥锁,那么所有的互斥锁本质都一样的

都是将并发编程变成串行,以此来控制同一时间内共享数据只能被一个任务所修改

进而来保证数据的安全,可以肯定的一点是:保护不同数据的安全,就应该加不同的锁

要想了解GIL,首先可以肯定一点的就是:每次执行一个py文件,都会产生一个独立的进程

比如运行1.py 2.py 3.py 就会开三个进程,而且是开三个不同的进程

在一个python的进程中,不仅是有主线程,还应该有开启的其他线程,比如垃圾回收机制级别的线程

但是这些线程都是在这个进程当中运行的,这个无需多言

而前面我们也提到了,线程之间的数据是共享的,既然数据是共享的

代码,其实本身也是数据,也是被所有线程共享的,这其中也包括解释器的代码

而程序在执行之前,需要先执行编译器的代码(这很好理解,否则你的代码仅仅是字符串)

那执行编译器的代码是不是也需要保证编译器的代码安全

所以为了保证代码的安全,我们在编译器上加了一把锁,这把锁就是GIL全局解释锁

而加了这把锁就意味着什么?就意味着python解释器同一时间只能执行一个任务代码

这样就不会出现垃圾回收代码和用户代码同时操作一个变量导致逻辑混乱的问题


GIL与Lock

那既然都已经有一把锁,来保证多线程只能一个一个运行的状态

那为什么还要有Lock这个方法呢?有过这个疑问吗?

还是那句话,加锁的目的是为了保护共享的数据,保证同一时间只能有一个线程来修改共享的数据

进而我们就应该得出结论:保护不同的数据就应该加不同的锁

那问题就变得很清晰了,GIL和Lock是两把锁,保护的数据是不一样的

前者保护的是解释器级别的代码,比如垃圾回收机制啊什么的

但是后面的则是保护的自己开发的应用程序的数据,GIL是不负责的

只能用户自己定义然后加锁处理

如果有100个线程来抢GIL锁

一定有一个线程A先抢到了GIL,然后就开始执行了,只要执行就会拿到lock.acquire()

很可能在A还没运行完,另一个线程B抢到了GIL锁,然后开始运行,看到lock没有被释放,于是就进行阻塞

阻塞的同时就会被迫交出GIL,直到A重新抢到GIL,从上次暂停的位置继续执行,直到正常释放互斥锁lock

举个例子吧:

from threading import Thread, Lock
import os, time


def work():
    global n
    lock.acquire()
    temp = n
    time.sleep(0.1)
    n = temp - 1
    lock.release()


if __name__ == '__main__':
    lock = Lock()
    n = 100
    l = []
    for i in range(100):
        t = Thread(target=work)
        l.append(t)
        t.start()
    for t in l:
        t.join()
    print(n)

打印的结果一定是 0 因为共享数据被保护了,只能一个一个执行


GIL与多线程

问题又出现了,进程呢,是可以利用多核,但是时间长,开销大

python的多线程开销是小,但是由于GIL的原因,不能利用多核优势

这就是在小节刚开始提的批判的‘不完美’之处

在解决这个问题之前,应该对一些问题达成共识!

CPU到底是干啥的呢?是用来计算的还是用来处理I/O阻塞的呢?

很明显是处理计算的,多个CPU是用来处理多个计算任务,换句话说,多个CPU是提高计算速度

但是当CPU遇到I/O阻塞的时候,还是需要等待的,所以,多CPU对处理阻塞没什么用

如果你的工厂是处理石材的,那工人越多效率越快(MC玩多了)

但是,如果你是等待石材过来再加工的,那等待的过程,有多少工人也没用

工人就是CPU,第一个例子就是计算密集型!第二个例子是I/O密集型!

从上面就可以看出来

对计算来说,CPU越多越好,但是对于I/O来说,再多的CPU也没用

但是,没有纯计算和纯I/O的程序,所以我们只能相对的去看一个程序到底是什么类型

所以解决问题是这样的:

方案一:开启多进程

方案二:开启多线程

单核

如果是计算密集型,没有多核的来并行计算,方案一增加了创建进程的开销

如果是I/O密集型,方案一创建的进程开销大,所以还是要选择方案二

多核

如果是计算密集型,在python中同一时刻只能一个线程执行,用不到多核,所以应该选择方案一

如果是I/O密集型,核就没用了,所以应该用方案二

但是很明显现在的计算机都是多核,python对于计算密集型的任务开多线程并不能提高效率

甚至有时候都比不上串行,但是要是对于I/O密集型任务,还是有显著提升的


性能测试

上面说的这么热闹,下面就来亲自试验一下

1.如果并发的多个任务是计算密集型:多进程效率高

from multiprocessing import Process
from threading import Thread
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) #耗时5s多
        p=Thread(target=work) #耗时18s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

如果并发的多个任务是I/O密集型:多线程效率高

from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
    time.sleep(2)
    print('===>')
if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) #本机为4核
    start=time.time()
    for i in range(400):
        # p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上
        p=Thread(target=work) #耗时2s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))
  1. 多线程用于IO密集型,如socket,爬虫,web
  2. 多进程用于计算密集型,如金融分析

*****
*****
原文地址:https://www.cnblogs.com/jevious/p/11410591.html