GIL、死锁与递归锁

 

一、互斥锁 

用互斥锁,目的:局部串行(保护自己的数据

  进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,竞争带来的结果就是错乱,如何控制,就是加锁处理(即局部实行串行)。

模拟抢票实例:

from multiprocessing import Process,Lock
import json,os,time,random

def search():
    with open('db.txt',encoding='utf-8')as f:
        dict = json.load(f)
        print('%s 剩余票数 %s'%(os.getpid(),dict['count']))

def get():
    with open('db.txt',encoding='utf-8') as reaf_f:
        dic = json.load(reaf_f)

    if dic['count']>0:
        dic['count'] -= 1
        time.sleep(random.randint(1,3))  # 模拟手速,网速
        with open('db.txt','w',encoding='utf-8') as write_f:
            json.dump(dic,write_f)
            print('%s 抢票成功' %os.getpid())
    else:
        print('剩余票数为%s,购票失败'%dic['count'])

def task(mutex):
    search()    # 20个人都可以并发的查询票数
    mutex.acquire()    # 加锁
    get()              #通过锁,查询到结果的20人通过竞争逐一买票。前一个释放锁后后一个才可以进入,即串行
    mutex.release()    # 解锁

if __name__ == '__main__':
    mutex = Lock()
    for i in range(20):  # 20个人抢票
        p = Process(target=task,args=(mutex,))
        p.start()

 

二、GIL互斥锁(保护解释器级别)
 
在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。为什么会这样呢?

1、python程序执行顺序

  如执行test.py程序,都会开启python一个进程,代码中包含多个线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问。

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

2、GIL锁介绍

  GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图主进程中开启了两个线程,由于线程1先拿到解释器的GIL锁,故向cpu传送指令执行此线程,若此线程在执行过程遇到IO阻塞,被解释器强行要求释放GIL锁,线程2进入解释器执行相应代码,等线程1再次拿到解释器的权限时,继续执行其剩余程序。

3.多线程与GIL

  有了GIL存在,同一时刻同一进程中只有一个线程被执行。但是多线程仍然存在它的意义,解释如下:

  案例:我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:

方案一:开启四个进程,方案二:一个进程下,开启四个线程。

#单核情况下,分析结果:
  如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
  如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

#多核情况下,分析结果:
  如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

 

  结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

 

应用(总结):
多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析
  1. 每个cpython进程内都有一个GIL
  2. GIL导致同一进程内多个进程同一时间只能有一个运行
  3. 之所以有GIL,是因为Cpython的内存管理不是线程安全的
  4. 对于计算密集型用多进程,多IO密集型用多线程

 

(1)计算密集型实例

  多线程:

from threading import Thread
import time
def work():
    res=0
    for i in range(100000000):
        res*=i
if __name__ == '__main__':
    start=time.time()
    result=[]
    for i in range(4):#因为4核,所以开启4个进程
        p=Thread(target=work)
        result.append(p)
        p.start()
    for p in result:
        p.join()
    end=time.time()
    print(end-start)  #24.468851566314697

  多进程:

from multiprocessing import Process
import time
def work():
    res=0
    for i in range(100000000):
        res*=i
if __name__ == '__main__':
    start=time.time()
    result=[]
    for i in range(4):#因为4核,所以开启4个进程
        p=Process(target=work)
        result.append(p)
        p.start()
    for p in result:
        p.join()
    end=time.time()
    print(end-start)  #15.74721097946167

   分析:对于多进程情况,所开4个进程分别在4个cpu上同时进行执行,所花时间为开启进程时间和单个进程运行时间的总和,对于多线程情况,其都在竞争解释器权限,排队进行执行代码,所化时间为四个线程的总和。多线程花费时间未与多进程花费时间呈4倍关系是因为开启进程开销比开启线程开销大很多。

(2)IO密集型实例

  多线程:

from threading import Thread
import time
def work():
   time.sleep(2)
if __name__ == '__main__':
    start=time.time()
    result=[]
    for i in range(400):
        p=Thread(target=work)
        result.append(p)
        p.start()
    for p in result:
        p.join()
    end=time.time()
    print(end-start)  #2.049804925918579

  多进程

from multiprocessing import Process
    import time
    def work():
        time.sleep(2)
    if __name__ == '__main__':
        start = time.time()
        result = []
        for i in range(400):
            p = Process(target=work)
            result.append(p)
            p.start()
        for p in result:
            p.join()
        end = time.time()
        print(end - start)  # 27.082503080368042

分析:对于多线程情况,线程遇到阻塞,便会切换到另个线程,所化总时间就为花时间最长的单个线程的时间,对于多进程情况,同时开启这么多进程会花费很多时间,其次单个进程执行遇到阻塞时,若没有其他任务也会继续等待阻塞结束。

 三、死锁与递归锁

   对于互斥锁,只能acquire一次锁

1、死锁现象

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

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('\033[41m%s 拿到A锁\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B锁\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B锁\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A锁\033[0m' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

'''
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
然后就卡住,死锁了
''

 

2、递归锁 (可以acquire多次)

  为解决上述死锁现象,出现了递归锁的概念:在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

from threading import Thread,RLock
import time
mutexA=mutexB=RLock()


class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('%s 拿到A锁' %self.name)

        mutexB.acquire()
        print('%s 拿到B锁' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('%s 拿到B锁' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('%s 拿到A锁' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()

分析:一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止。