Python线程的常见的lock

IO阻塞分析:

下面该需求很简单将一个数值100做自减处到0.主函数中有0.1秒的IO阻塞

import threading
import time
def sub():
    global num   #  掌握为什么加global num
    temp=num
    time.sleep(0.1)
    num=temp-1

    time.sleep(2)
num=100
l=[]
for i in range(100):       
    t=threading.Thread(target=sub,args=())
    t.start()
    l.append(t)
for t in l:        #100个线程的IO阻塞
    t.join()
print(num)

"D:Program Filespython.exe" E:/py_code/多线程/互斥锁.py
99

Process finished with exit code 0


分析:
通过代码我们可以看到因为主函数中增加了一段IO阻塞的代码,所以我们考虑用到了线程。
1.因为Python GIL的特性原因,对于多线程并不能真正的实现并行而只能并发
2.当第一个线程抢到了GIL后(temp=100),其他的线程暂时不可以执行,但当第一个线程执行到time.sleep(0.1)时(IO阻塞),此时cup是闲置的,第二个线程可以抢GIL。

3.因为第一个线程取到temp=100还没有执行到num=temp-1所以此时第二个线程取到的值还是100,接着也执行到time.sleep(0.1),同理第三个线程抢到GIL。依次类推可以以知道这一百个线程在执行到time.sleep(0.1)之前得到的值都是100,当0.1秒过后全部都依次执行了num=temp-1,所以得出的结果是99.

所以很显然上面的代码没办法得到我们想要的结果,造成的原因就是IO阻塞和GIL相互作用的原因,所以下面会有另外的途径去解决

互斥锁


import threading
import time
def sub():
    global num   #  掌握为什么加global num
    lock.acquire()    #加锁直到释放锁下个线程才可以抢锁
    temp=num
    time.sleep(0.1)
    num=temp-1
    lock.release()    #释放锁下个线程可以抢锁了
    time.sleep(2)
num=100
l=[]
lock=threading.Lock()   #生成锁对象
for i in range(100):       #生成100个线程
    t=threading.Thread(target=sub,args=())
    t.start()
    l.append(t)
for t in l:        #100个线程的IO阻塞
    t.join()
print(num)


"D:Program Filespython.exe" E:/py_code/多线程/互斥锁.py
0

Process finished with exit code 0

分析:

1.全局定义一把锁,因为进程处理的这个数据num是个公共数据,100个线程进行处理
2.主程序中处理数据的代码只有下面的这三行:

temp=num
time.sleep(0.1)
num=temp-1

所以将这三行代码锁起来,使其在执行完之前其他的线程无法进行干预和取值执行。

3.所以将代码前加锁,数据做完运算后释放锁。当锁释放后其他线程就可以去抢占这把锁。

4.线程的运行流程是:
第一个线程抢到GIL,并拿到值temp=100,开始执行代码,当执行到lock.acquire() 时,下面的代码加锁运行,一直执行到time.sleep(0.1),遇到IO阻塞,第二个线程开始抢GIL,抢到后执行代码执行到lock.acquire(),此时该锁已经被第一个线程抢占还未释放,所以第二个线程只能等第一个线程释放。

5.当0.1s过后第一个线程继续执行代码num=temp-1,此时temp=99,接着执行到lock.release(),锁释放第二个线程抢到开始执行temp=num,而此时的temp=99,接着执行到time.sleep(0.1),第三个线程抢占GIL,但因为第二个线程未释放定义锁,所以无法继续执行,此时第二个线程执行num=temp-1,此时的num=98.依此类推接下来的线程均依据进行运行。所以最后的执行结果等于0

递归锁

死锁:

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

import threading
import time
class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
    def run(self):
        self.foo()
        self.bar()
    def foo(self):
        LockA.acquire()
        print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
        LockB.acquire()
        print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
        LockB.release()
        LockA.release()
    def bar(self):
        #time.sleep(2)
        LockB.acquire()
        print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
        LockA.acquire()
        print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
        LockA.release()
        LockB.release()
LockA=threading.Lock()
LockB=threading.Lock()
for i in range(10):
    t=MyThread()
    t.start()

"D:Program Filespython.exe" E:/py_code/多线程/互斥锁.py
I am Thread-1 GET LOCKA---->Tue Jul 18 18:46:27 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 18:46:27 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 18:46:27 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 18:46:27 2017

分析:
1.上述的代码定义了两把锁LockA和LockB,并定义一个类,类中继承线程,则MyThread中就可以调用线程的所有方法。

2.在foo函数中在A锁中嵌套了B锁这这里第一个线程进入执行的逻辑是,执行第一个函数foo,执行第一个打印然后启动B锁执行第二个打印释放B锁释放A锁进入到bar函数,于此同时第二个线程进入到要执行foo,当第一个线程释放A锁的那一刻,第二个线程马上取到A锁,但是此时第一个线程已经进入到bar函数也同一时刻取到B锁执行第一个bar函数的第一个打印,当其执行到启动A锁时就会卡住,因为此时的A锁已经被第二个线程拿到,同理第二个线程此时在foo函数中想启动B锁,但是B锁已经被第一个线程拿到,所以这个时候整个进程就会卡住

3.这是在互斥锁中常见的问题,所以我们需要引进第二把锁递归锁:

递归锁

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

import threading
import time
class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
    def run(self):
        self.foo()
        self.bar()
    def foo(self):
        RLock.acquire()
        print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
        RLock.acquire()
        print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
        RLock.release()
        RLock.release()
    def bar(self):

        RLock.acquire()
        print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
        RLock.acquire()
        print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
        RLock.release()
        RLock.release()

RLock=threading.RLock()
for i in range(10):
    t=MyThread()
    t.start()



 "D:Program Filespython.exe" E:/py_code/多线程/互斥锁.py
I am Thread-1 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKA---->Tue Jul 18 19:19:17 2017

Process finished with exit code 0

分析:

1.上述的代码在全局定义了一把递归锁

2.当第一个线程进入执行时先进入到foo函数,启动第一把锁并记录一次接着执行第一个打印,然后启动第二把锁执行第二次打印并释放两把锁。此时包括第一个线程在内这10个线程都能进行抢锁,这里RLock采取的就近原则,所以还是第一个线程抢到并进入到bar函数,此时其他线程是抢不到锁只能继续等待线程一释放,线程一顺序执行完bar函数并释放RLock锁,此刻线程一已经完整的执行完整个工作,并且线程2抢到锁执行过程和线程一相同。

3.依次类推这10个线程会先后执行完各自的工作。如上述代码执行的结果所示

信号量

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

import threading
import time
semaphore=threading.Semaphore(5)

def foo():
    semaphore.acquire()
    time.sleep(2)
    print("ok")
    semaphore.release()


for i in range(100):
    t=threading.Thread(target=foo,args=())
    t.start()
    

分析:

1.全局定义一个信号量,并限制最大接入的线程数为5个

2.执行逻辑,当第一个线程进入后semaphore内置计数减1,则这里可以接收4个线程,此时第一个线程打印第一个OK,接着第二个线程进入打印第二个OK,一直到第五个线程进入后,这时第六个线程在第五个线程没有执行完semaphore.release()时是不可以进入的,所以按照这个逻辑,该端代码的执行结果是每5个OK打印一次一共打印100个OK.

原文地址:https://www.cnblogs.com/lijian-22huxiaoshan/p/7202512.html