Python | 浅谈并发锁与死锁问题

本文始发于我的公众号:TechFlow,原创不易,求个关注程序员


今天是Python专题的第24篇文章,咱们一块儿来聊聊多线程场景当中不可或缺的另一个部分——web

若是你学过操做系统,那么对于锁应该不陌生。锁的含义是线程锁,能够用来指定某一个逻辑或者是资源同一时刻只能有一个线程访问。这个很好理解,就好像是有一个房间被一把锁锁住了,只有拿到钥匙的人才能进入。每个人从房间门口拿到钥匙进入房间,出房间的时候会把钥匙再放回到门口。这样下一个到门口的人就能够拿到钥匙了。这里的房间就是某一个资源或者是一段逻辑,而拿取钥匙的人其实指的是一个线程。编程

加锁的缘由

咱们明白了锁的原理,不由有了一个问题,咱们为何须要锁呢,它在哪些场景当中会用到呢?多线程

其实它的使用场景很是广,咱们举一个很是简单的例子,就是淘宝买东西。咱们都知道商家的库存都是有限的,卖掉一个少一个。假如说当前某个商品库存只剩下一个,但当下却有两我的同时购买。两我的同时购买也就是有两个请求同时发起购买请求,若是咱们不加锁的话,两个线程同时查询到商品的库存是1,大于0,进行购买逻辑以后,减一。因为两个线程同时执行,因此最后商品的库存会变成-1。并发

显然商品的库存不该该是一个负数,因此咱们须要避免这种状况发生。经过加锁能够完美解决这个问题。咱们规定一次只能有一个线程发起购买的请求,那么这样当一个线程将库存减到0的时候,第二个请求就没法修改了,就保证了数据的准确性。编辑器

代码实现

那么在Python当中,咱们怎么样来实现这个锁呢?分布式

其实很简单,threading库当中已经为咱们提供了线程的工具,咱们直接拿过来用就能够了。咱们经过使用threading当中的Lock对象, 能够很轻易的实现方法加锁的功能。工具

import threading

class PurchaseRequest:
    '''
    初始化库存与锁
    '''

    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._lock = threading.Lock()

    def incr(self,delta=1):
        '''
        加库存
        '''

        self._lock.acquire()
        self._value += delta
        self._lock.release()

    def decr(self,delta=1):
        '''
        减库存
        '''

        self._lock.acquire()
        self._value -= delta
        self._lock.release()

咱们从代码当中就能够很轻易的看出Lock这个对象的使用方法,咱们在进入加锁区(资源抢占区)以前,咱们须要先使用lock.acquire()方法获取锁。Lock对象能够保证同一时刻只能有一个线程获取锁,只有获取了锁以后才会继续往下执行。当咱们执行完成以后,咱们须要把锁“放回门口”,因此须要再调用一下release方法,表示锁的释放。测试

这里有一个小问题是不少程序员在编程的时候老是会忘记release,致使没必要要的bug,并且这种分布式场景当中的bug很难经过测试发现。由于测试的时候每每很难测试并发场景,code review的时候也很容易忽略,所以一旦泄露了仍是挺难发现的。ui

为了解决这个问题,Lock还提供了一种改进的用法,就是使用with语句。with语句咱们以前在使用文件的时候用到过,使用with能够替咱们完成try catch以及资源回收等工做,咱们只管用就完事了。这里也是同样,使用with以后咱们就能够不用管锁的申请和释放了,直接写代码就行,因此上面的代码能够改写成这样:

import threading

class PurchaseRequest:
    '''
    初始化库存与锁
    '''

    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._lock = threading.Lock()

    def incr(self,delta=1):
        '''
        加库存
        '''

  with self._lock:
         self._value += delta

    def decr(self,delta=1):
        '''
        减库存
        '''

        with self._lock:
         self._value -= delta

这样看起来是否是清爽不少?

可重入锁

上面介绍的只是最简单的锁,咱们常用的每每是可重入锁

什么叫可重入锁呢?简单解释一下,就是在一个线程已经持有了锁的状况下,它能够再次进入被加锁的区域。可是既然线程还持有锁没有释放,那么它不该该仍是在加锁区域吗,怎么会有须要再次进入被加锁区域的状况呢?实际上是有的,道理也很简单,就是递归

咱们把上面的例子稍微改一点点,就彻底不同了。

import threading

class PurchaseRequest:
    '''
    初始化库存与锁
    '''

    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._lock = threading.Lock()

    def incr(self,delta=1):
        '''
        加库存
        '''

  with self._lock:
         self._value += delta

    def decr(self,delta=1):
        '''
        减库存
        '''

        with self._lock:
         self.incr(-delta)

咱们关注一下上面的decr方法,咱们用incr来代替了本来的逻辑实现了decr。可是有一个问题是decr也是一个加锁的方法,须要前一个锁释放了才能进入。但它已经持有了锁了,那么这种状况下就会发生死锁

咱们只须要把Lock换成可重入锁就能够解决这个问题,只须要修改一行代码。

import threading

class PurchaseRequest:
    '''
    初始化库存与锁
    咱们使用RLock代替了Lock,也可重入锁代替了普通锁
    '''

    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._lock = threading.RLock()

    def incr(self,delta=1):
        '''
        加库存
        '''

  with self._lock:
         self._value += delta

    def decr(self,delta=1):
        '''
        减库存
        '''

        with self._lock:
         self.incr(-delta)

总结

今天咱们的文章介绍了Python当中锁的使用方法,以及可重入锁的概念。在并发场景下开发和调试都是一个比较困难的工做,稍微不当心就会踩到各类各样的坑,死锁只是其中一种比较常见而且比较容易解决的问题,除此以外还有不少其余各类各样的问题。

针对死锁的问题,Python还提供了其余的解决方案,咱们放到下一篇文章当中再和你们分享。

今天的文章到这里就结束了,若是喜欢本文的话,请来一波素质三连,给我一点支持吧(关注、转发、点赞)。

- END -

相关文章
相关标签/搜索