一篇文章理清Python多线程同步锁,死锁和递归锁

前面说到过python多线程的基本使用,大概的内容有几点python

1.建立线程对象
t1 = threading.Thread(target=say,args=('tony',))
2.启动线程
t1.start()
后面又说了两个点就是join和守护线程的概念
复制代码

可是不知道你们有没有注意到一点就是前面说的两个功能是相互独立的,相互不干涉的,不会用到同享的资源或者数据,若是咱们多个线程要用到相同的数据,那么就会存在资源争用和锁的问题,无论在什么语言中,这个都是不能避免的。对数据库属性的同窗应该也了解,数据库中也存在锁的概念。数据库

今天这篇文章咱们说说python多线程中的同步锁,死锁和递归锁的使用。bash

  1. Python同步锁

锁一般被用来实现对共享资源的同步访问。为每个共享资源建立一个Lock对象,当你须要访问该资源时,调用acquire方法来获取锁对象(若是其它线程已经得到了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁。多线程

下面咱们来举个例子说明若是多线程在没有同步锁的状况下访问公共资源会致使什么状况app

import threading
import time

num = 100

def fun_sub():
    global num
    # num -= 1
    num2 = num
    time.sleep(0.001)
    num = num2-1

if __name__ == '__main__':
    print('开始测试同步锁 at %s' % time.ctime())

    thread_list = []
    for thread in range(100):
        t = threading.Thread(target=fun_sub)
        t.start()
        thread_list.append(t)

    for t in thread_list:
        t.join()
    print('num is %d' % num)
    print('结束测试同步锁 at %s' % time.ctime())
复制代码

上面的例子其实很简单就是建立100的线程,而后每一个线程去从公共资源num变量去执行减1操做,按照正常状况下面,等到代码执行结束,打印num变量,应该获得的是0,由于100个线程都去执行了一次减1的操做。性能

可是结果却不是咱们想一想的,咱们看看结果测试

开始测试同步锁 at Sun Apr 28 09:56:45 2019
num is 91
结束测试同步锁 at Sun Apr 28 09:56:45 2019
复制代码

咱们会发现,每次执行的结果num值都不是同样的,上面显示的是91,那就存在问题了,为何结果不是0呢?ui

咱们来看看上面代码的执行流程。
1.由于GIL,只有一个线程(假设线程1)拿到了num这个资源,而后把变量赋值给num2,sleep 0.001秒,这时候num=100
2.当第一个线程sleep 0.001秒这个期间,这个线程会作yield操做,就是把cpu切换给别的线程执行(假设线程2拿到个GIL,得到cpu使用权),线程2也和线程1同样也拿到num,返回赋值给num2,然sleep,这时候,其实num仍是=100.
3.线程2 sleep时候,又要yield操做,假设线程3拿到num,执行上面的操做,其实num有可能仍是100
4.等到后面cpu从新切换给线程1,线程2,线程3上执行的时候,他们执行减1操做后,其实等到的num其实都是99,而不是顺序递减的。
5.其余剩余的线程操做如上
复制代码

你们应该发现问题了,结果和咱们想一想的不同,那咱们怎么才能等到咱们想要的结果呢?就是100个线程操做num变量获得最后结果为0?spa

这里就要借助于python的同步锁了,也就是同一时间只能放一个线程来操做num变量,减1以后,后面的线程操做来操做num变量。看看下面咱们怎么实现。线程

import threading
import time

num = 100

def fun_sub():
    global num
    lock.acquire()
    print('----加锁----')
    print('如今操做共享资源的线程名字是:',t.name)
    num2 = num
    time.sleep(0.001)
    num = num2-1
    lock.release()
    print('----释放锁----')

if __name__ == '__main__':
    print('开始测试同步锁 at %s' % time.ctime())

    lock = threading.Lock() #建立一把同步锁

    thread_list = []
    for thread in range(100):
        t = threading.Thread(target=fun_sub)
        t.start()
        thread_list.append(t)

    for t in thread_list:
        t.join()
    print('num is %d' % num)
    print('结束测试同步锁 at %s' % time.ctime())
复制代码

看到上面咱们给中间的减1代码块,加个一把同步锁,这样,咱们就能够获得咱们想要的结果了,这就是同步锁的做用,一次只有一个线程操做同享资源。

看看上面代码执行的结果:

.......
----加锁----
如今操做共享资源的线程名字是: Thread-98
----释放锁----
----加锁----
如今操做共享资源的线程名字是: Thread-100
----释放锁----
num is 0
结束测试同步锁 at Sun Apr 28 12:08:27 2019
复制代码
  1. Python死锁

死锁的这个概念在不少地方都存在,比较在数据中,大概介绍下私有是怎么产生的 1.A拿了一个苹果 2.B拿了一个香蕉 3.A如今想再拿个香蕉,就在等待B释放这个香蕉 4.B同时想要再拿个苹果,这时候就等待A释放苹果 5.这样就是陷入了僵局,这就是生活中的死锁

python中在线程间共享多个资源的时候,若是两个线程分别占有一部分资源而且同时等待对方的资源,就会形成死锁,由于系统判断这部分资源都正在使用,全部这两个线程在无外力做用下将一直等待下去。下面是一个死锁的例子:

import threading
import time

lock_apple = threading.Lock()
lock_banana = threading.Lock()

class MyThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):

        lock_apple.acquire()  # 若是锁被占用,则阻塞在这里,等待锁的释放

        print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))

        lock_banana.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        lock_banana.release()
        lock_apple.release()


    def fun2(self):

        lock_banana.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        time.sleep(0.1)

        lock_apple.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))
        lock_apple.release()

        lock_banana.release()

if __name__ == "__main__":
    for i in range(0, 10):  #创建10个线程
        my_thread = MyThread()  #类继承法是python多线程的另一种实现方式
        my_thread.start()
代码执行hung住,死锁了

线程 Thread-1 , 想拿: 苹果--Sun Apr 28 12:21:06 2019
线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
线程 Thread-2 , 想拿: 苹果--Sun Apr 28 12:21:06 2019

Process finished with exit code -1
复制代码

上面的代码其实就是描述了苹果和香蕉的故事。你们能够仔细看看过程。下面咱们看看执行流程

1.fun1中,线程1先拿了苹果,而后拿了香蕉,而后释放香蕉和苹果,而后再在fun2中又拿了香蕉,sleep 0.1秒。 2.在线程1的执行过程当中,线程2进入了,由于苹果被线程1释放了,线程2这时候得到了苹果,而后想拿香蕉 3.这时候就出现问题了,线程一拿完香蕉以后想拿苹果,返现苹果被线程2拿到了,线程2拿到苹果执行,想拿香蕉,发现香蕉被线程1持有了 4.双向等待,出现死锁,代码执行不下去了

上面就是大概的执行流程和死锁出现的缘由。在这种状况下就是在同一线程中屡次请求同一资源时候出现的问题。

  1. Python递归锁RLock

为了支持在同一线程中屡次请求同一资源,python提供了"递归锁":threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源能够被屡次acquire。直到一个线程全部的acquire都被release,其余的线程才能得到资源。

下面咱们用递归锁RLock解决上面的死锁问题

import threading
import time

lock = threading.RLock()  #递归锁


class MyThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):

        lock.acquire()  # 若是锁被占用,则阻塞在这里,等待锁的释放

        print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))

        lock.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        lock.release()
        lock.release()


    def fun2(self):

        lock.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        time.sleep(0.1)

        lock.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))
        lock.release()

        lock.release()

if __name__ == "__main__":
    for i in range(0, 10):  #创建10个线程
        my_thread = MyThread()  #类继承法是python多线程的另一种实现方式
        my_thread.start()
复制代码

上面咱们用一把递归锁,就解决了多个同步锁致使的死锁问题。你们能够把RLock理解为大锁中还有小锁,只有等到内部全部的小锁,都没有了,其余的线程才能进入这个公共资源。

另一点前面没有就算用类继承的方法实现python多线程,这个你们能够查下,就算继承Thread类,而后从新run方法来实现。

最后你们可能还有个疑问,就算若是咱们都加锁了,也就是单线程了,那咱们还要开多线程有什么用呢?这里解释下,在访问共享资源的时候,锁是必定要存在了,可是咱们的代码中不是老是在访问公共资源的,还有一些其余的逻辑可使用多线程,因此咱们在代码里面加锁的时候,要注意在什么地方加,对性能的影响最小,这个就靠对逻辑的理解了。

好了今天就说到这里,我的意见,望指教。

相关文章
相关标签/搜索