Python 线程锁

这里锁涉及到死锁,互斥锁,以及全局GIL锁

先看看死锁现象:

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

import time

from threading import Thread
from threading import Lock


lock_A = Lock()
lock_B = Lock()


class MyThread(Thread):

    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        lock_A.acquire()
        print(f'{self.name}拿到A锁')

        lock_B.acquire()
        print(f'{self.name}拿到B锁')
        lock_B.release()

        lock_A.release()

    def f2(self):
        lock_B.acquire()
        print(f'{self.name}拿到B锁')
        time.sleep(0.1)

        lock_A.acquire()
        print(f'{self.name}拿到A锁')
        lock_A.release()

        lock_B.release()

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

    print('主....')

#输出
Thread-1 拿到了A锁
Thread-1 拿到了B锁
Thread-1 拿到了B锁
Thread-2 拿到了A锁

 过程分析:开启3个线程,启动时执行run方法,即f1(), 线程1拿到A锁,然后又拿到B锁,在此期间其它两个线程都被阻塞住。线程1释放B锁,但此时其它线程仍然在等待,因为得先拿到A锁,才能拿B锁,然后A锁被 释放.接着执行B锁,接着向下执行,拿到B锁,但此时线程二进来,拿到A锁。就出现了线程1要拿A锁,线程2要拿B锁,但都拿不到的现象,即死锁。

 

递归锁:

为了解决上面的死锁问题,python中引入了Rlock递归锁

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁

import time

from threading import Thread
from threading import RLock


lock_A = lock_B = RLock()


class MyThread(Thread):

    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        lock_A.acquire()
        print(f'{self.name}拿到A锁')

        lock_B.acquire()
        print(f'{self.name}拿到B锁')
        lock_B.release()

        lock_A.release()

    def f2(self):
        lock_B.acquire()
        print(f'{self.name}拿到B锁')
        time.sleep(0.1)

        lock_A.acquire()
        print(f'{self.name}拿到A锁')
        lock_A.release()

        lock_B.release()


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

    print('主....')

#输出:
Thread-1拿到A锁
Thread-1拿到B锁
Thread-1拿到B锁
主....
Thread-1拿到A锁
Thread-3拿到A锁
Thread-3拿到B锁
Thread-3拿到B锁
Thread-3拿到A锁
Thread-2拿到A锁
Thread-2拿到B锁
Thread-2拿到B锁
Thread-2拿到A锁

注意:一定要写成:lock_A = lock_B = RLock()

当第一个线程拿到锁A时,其它线程都等着,然后counter +1 ,拿到B锁又加1,然后依次又释放,最后counter又变成0了。这时三个线程又开始抢,然后拿到了B锁,其它线程都等着,counter+1, 接着拿A锁,又依次释放,最后counter变成0,所以三个线程最后都拿到过锁。

 

信号量:其实也是一种锁

Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()

import time
import random

from threading import Thread
from threading import Semaphore
from threading import current_thread


sem = Semaphore(5)

def go_public_wc():
    sem.acquire()
    print(f'{current_thread().getName()} 上厕所ing')
    time.sleep(random.randint(1,3))
    sem.release()


if __name__ == '__main__':
    for i in range(20):
        t = Thread(target=go_public_wc)
        t.start()

它用来限制线程数量 ,允许多个线程来抢锁,但同时只允许指定数量的线程(上面是5个),出来一个进去一个,同一时刻只允许有5个。

 

GIL锁

所谓全局解释器锁,当如上面所示的有多个cpu时,只能用一个cpu,当代码被 加载写到文件,然后加载到编译器的过程中会给它上锁,只允许一个线程进来,然后代码被编译成字节码,然后通过虚拟机转成机器码,最后调用cpu执行。

那么为什么要在这加锁呢?

一方面当时 开发时都是单核时代,另一方面,如果不加锁那么就需要在解释器内部不断加锁释放锁,且容易出现死锁现象。开发程序员为了方便就直接加了全局锁,只允许一个线程进入解释器。 (但当遇到I/O时,cpu会将线程挂起,GIL锁会被释放,此时就可以做到并发。)

加锁保证了cpython解释器的数据资源的安全,缺点也很明显,单个进程的多线程不能利用多核。当遇到偏计算的程序时,因为无法利用多核的优势,相对要差一些,但如果是多I/O类型的程序,遇到I/O操作时,操作系统会强制将cpu切走,多线程就可以并发,cpu快速调度线程,然后线程执行I/O操作,这种情况下就不会有什么差距了。

总结:

单个进程的多线程无法并行,不能利用多核,但可以并发。

多个进程可以并发,并行。

 

GIL锁与Lock的区别:

相同点:都是互斥锁,

不同点:

GIL全局解释器锁保护解释器内全局数据安全,自己定义的互斥锁保证进程(线程)内的数据安全。

GIL全局解释器锁不需要手动创建释放,自己定义的解释器锁需要手动创建,释放。

上一篇:Python 线程

下一篇:Python 进程池与线程池