多线程死锁问题

多线程中的锁问题

在多线程同步使用的时候存在数据的不安全问题,python解释器在底层添加了GIL全局解释器锁机制来控制锁的安全问题,但不是绝对的安全。在日常的开发中还需要开发者添加锁机制在代码中。

一、多线程之间的数据安全问题

(一)、 '+='、'-='、'*='、'/='、if语句和while语句:不安全

  • 原因:python中每执行700条CPU执行GIL锁就会出现轮转的情况,而+=、-=、*= 都是执行的两部操作,第一步运算,第二步赋值,在赋值之前因为GIL锁轮转导致数据还没有赋值就已经失去资源,从而造成当前线程失去锁的保护,数据不安全。
  • 这些都是操作的全局变量,只要出现了两步以上(包括两步)都是不安全的。
import dis
print(dis.dis(n))
# 通过dis模块可以查看变量n在运算过程中程序步骤对应的每一个底层的操作系统指令。
0 SETUP_LOOP              24 (to 26)
            2 LOAD_GLOBAL              0 (range) # 加载全局变量n到CPU
            4 LOAD_CONST               1 (100001) # 加载1到CPU中
            6 CALL_FUNCTION            1 # 调用函数
            8 GET_ITER          
      >>   10 FOR_ITER                12 (to 24)
      ############################################
      # 注意:python解释器中的GIL锁很有可能在线程执行到这里的时候锁机制失效,GIL锁轮转,造成另外一个变量获取到全局变量资源进行处理,两个线程都在对同一个变量处理,从而数据变得不安全。
           12 STORE_FAST               0 (i)
      # 如果发生了GIL锁失效的问题,当该线程重新获取到CPU后,全局变量已经变了。
  • 示例:
import time
from threading import Thread
import dis

def add():
    global n
    for i in range(100001):
        n += 1

def sub():
    global n
    for i in range(100001):
        n -= 1

if __name__ == '__main__':
    n = 0
    t_list = []
    for i in range(2):
        t1 = Thread(target=add)
        t1.start()
        t2 = Thread(target=sub)
        t2.start()
        t_list.append(t1)
        t_list.append(t2)
    [t.join() for t in t_list]
    print(n)
    print(dis.dis(add))

'''结果是:
59487或者任意的一个数,不是应该的结果0,说明在大量的运算下锁机制失效了
'''

(二)、append()和pop()

  • append()、pop()、strip()等列表、字符串和字典的操作:数据是安全的
  • 原因:使用append()和pop()的数据类型是可变的,可变的数据类型都有一个独立的内存空间,虽然其他的程序可以调用和修改数据,但是在程序代码中只是通过数据的内存地址来进行调用,如果GIL锁轮转发生在调用内存地址之前数据还没有使用,如果发生在append()和pop()后,程序已经对数据完成了操作,也不会产生任何的影响。
import time
from threading import Thread
import dis

def add():
    for i in range(400000):
        n.append(1)

def sub():
    for i in range(400000):
        if not n:
            time.sleep(3)
        n.pop()
        '''
        Exception in thread Thread-2:
        Traceback (most recent call last):
          File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
            self.run()
          File "/usr/lib/python3.6/threading.py", line 864, in run
            self._target(*self._args, **self._kwargs)
          File "锁.py", line 41, in sub
            n.pop()
        IndexError: pop from empty list

        '''

if __name__ == '__main__':
    n = []
    t_list = []
    for i in range(2):
        t1 = Thread(target=add)
        t1.start()
        t2 = Thread(target=sub)
        t2.start()
        t_list.append(t1)
        t_list.append(t2)
    [t.join() for t in t_list]
    print(n)
'''
结果是:[]
'''

(三)、多线程下的单例类

  • 单里类:在多线程下是不安全的
  • 原因:单例类需要通过__new__()方法来实现,只开辟一块内存空间给实例化对象, 但是开辟之前需要用到if判断,如果if判断条件满足的情况下,准备执行开辟空间,但是突然出现阻塞,CPU执行其他的线程去了,其他线程执行完后继续当前线程,那么就会出现不止开一个内存空间的问题。
  • 解决办法:需要在单例类中导入from threading import Lock,实例化Lock对象,if语句之前添加with cls.lock_obj: ,注意这些操作必须全部在__new__()构造方法内完成,不能放在类外
import time
from threading import Thread
'''单例类'''
class A:
    __isinstance = None
    def __new__(cls, *args, **kwargs):
        if not cls.__isinstance:
            time.sleep(0.01)
            cls.__isinstance = super().__new__(cls)
        return cls.__isinstance

def func():
    a = A()
    print(a)

if __name__ == '__main__':

    for i in range(10):
        t = Thread(target=func)
        t.start()

'''结果是:
<__main__.A object at 0x7f13d39edac8>
<__main__.A object at 0x7f13d1d13240>
<__main__.A object at 0x7f13d1d22a20>
<__main__.A object at 0x7f13d1d13240>
<__main__.A object at 0x7f13d1d2ae10>
<__main__.A object at 0x7f13d1d45320>
<__main__.A object at 0x7f13d1d2ae10>
<__main__.A object at 0x7f13d1d45320>
<__main__.A object at 0x7f13d1c8a1d0>
<__main__.A object at 0x7f13d1c8a128>
'''
  • 单例类加锁解决数据不安全问题
import time
from threading import Thread
'''单例类'''
class A:
    '''实例化Lock对象'''
    from threading import Lock
    l = Lock()
    # 注意:实例化必须在类中,构造方法之前
    __isinstance = None
    def __new__(cls, *args, **kwargs):
        with cls.l:
          # 调用类中定义的锁对象,必须使用cls
            if not cls.__isinstance:
                time.sleep(0.01)
                cls.__isinstance = super().__new__(cls)
        return cls.__isinstance

def func():
    a = A()
    print(a)

if __name__ == '__main__':

    for i in range(10):
        t = Thread(target=func)
        t.start()

总结:针对数据不安全的问题可以通过加锁就可以解决,如果希望多线程中不出现数据不安全的问题,除了上边的情况,也需要注意尽可能不对全局变量和类的静态字段进行操作。

二、锁机制

  • 多线程锁用到的是Lock和RLock模块
    • 获取钥匙:acquire()
    • 归还钥匙:release()
  • 互斥锁:Lock
    • 一个线程中只能锁一次,一个钥匙开一把锁,只开一次锁就可以获取所有的资源
    • 效率高
  • 递归锁:RLock
    • 递归锁使用的时候获取钥匙后,需要开多次锁,锁里嵌套锁,所以需要多次acquire(),几次acruire()就需要几次release(),这一点需要注意,否则无法归还钥匙。
    • 递归锁相对互斥锁效率不是很高
import time
from threading import Thread, Lock, RLock
import dis

def add(l):
    global n
    l.acquire()
    for i in range(100001):
        n += 1
    l.release()

def sub(l):
    l.acquire()
    global n
    for i in range(100001):
        n -= 1
    l.release()

if __name__ == '__main__':
    l = Lock()
    n = 0
    t_list = []
    for i in range(2):
        t1 = Thread(target=add, args=(l,))
        t1.start()
        t2 = Thread(target=sub, args=(l,))
        t2.start()
        t_list.append(t1)
        t_list.append(t2)
    [t.join() for t in t_list]
    print(n)
'''
结果是:0
'''

特别注意:多线程中只要出现if或者while语句,数据一定是不安全的,必须在语句之前添加with lock_obj:完成加锁,这个和普通的加锁有一点区别。

def sub(l):
    for i in range(400000):
        with l:
            if not n:
                time.sleep(3)
            n.pop()

三、死锁问题

  • 原因:
    在开发过程中使用多线程,线程之间共享资源,如果两个线程分别占有一部分资源并且等待对方归还资源,那么就会造成死锁锁。死锁情况很少见,但是一旦出现就会造成程序停止运行,不做任何事情。
from threading import Thread, Lock
import time

class Mythread(Thread):
    def __init__(self):
        super().__init__()

    def run(self):
        if lockA.acquire():
            # 如果可以获取到锁,返回的是True,这是一个bool类型的
            print(self.name + '获取了A锁')
            # time.sleep(0.1)
            if lockB.acquire():
                print(self.name + '又获取了B锁,原来还有A锁')
                lockB.release()
            lockA.release()

class Mythread1(Thread):
    def __init__(self):
        super().__init__()

    def run(self):
        if lockB.acquire():
            # 如果可以获取到锁,返回的是True,这是一个bool类型的
            print(self.name + '获取了B锁')
            # time.sleep(0.1)
            if lockA.acquire():
                print(self.name + '又获取了A锁,原来还有B锁')
                lockA.release()
            lockB.release()

if __name__ == '__main__':
    lockA = Lock()
    lockB = Lock()
    m = Mythread()
    m.start()
    m1 = Mythread1()
    m1.start()

  • 解决办法:
    • 重构代码,修改逻辑,但是这种方比较耗时,效率不高
    • 互斥锁:在第一个线程中试图获取另外一个锁的acquire中添加timeout=秒数,来设定线程试图获取另外一个锁多久,超出时间自动终止,这样就另外一个线程就可以获取到锁,不会出现死锁的情况。
  from threading import Thread, Lock
  import time

  class Mythread(Thread):
      def __init__(self):
          super().__init__()

      def run(self):
          if lockA.acquire():
              # 如果可以获取到锁,返回的是True,这是一个bool类型的
              print(self.name + '获取了A锁')
              # time.sleep(0.1)
              if lockB.acquire(timeout=5):
                  # 设定等待时间,超出时间释放锁,解决死锁的问题
                  print(self.name + '又获取了B锁,原来还有A锁')
                  lockB.release()
              lockA.release()

  class Mythread1(Thread):
      def __init__(self):
          super().__init__()

      def run(self):
          if lockB.acquire():
              # 如果可以获取到锁,返回的是True,这是一个bool类型的
              print(self.name + '获取了B锁')
              # time.sleep(0.1)
              if lockA.acquire():
                  print(self.name + '又获取了A锁,原来还有B锁')
                  lockA.release()
              lockB.release()

  if __name__ == '__main__':
      lockA = Lock()
      lockB = Lock()
      m = Mythread()
      m.start()
      m1 = Mythread1()
      m1.start()
  • 递归锁:出现死锁的时候,可以把不同的锁全部变成一把递归锁,获取一把锁就可以开所有嵌套的锁,注意:需要多次使用acquire()
    • 死锁情况:
import time
from threading import Thread, RLock, Lock


def p1(name, lock1, lock2):
    lock1.acquire()
    print('%s获得了汽车' % name)
    time.sleep(1)
    lock2.acquire()
    print('%s获得了摩托车' % name)
    lock1.release()
    print('%s归还了汽车' % name)
    lock2.release()
    print('%s归还了摩托车' % name)

def p2(name, lock1, lock2):
    lock2.acquire()
    print('%s获得了汽车' % name)
    time.sleep(1)
    lock1.acquire()
    print('%s获得了摩托车' % name)
    lock2.release()
    print('%s归还了汽车' % name)
    lock1.release()
    print('%s归还了摩托车' % name)

if __name__ == '__main__':
    lock1 = Lock()
    lock2 = Lock()
    t1 = Thread(target=p1, args=('莉莉', lock1, lock2))
    t1.start()
    t2 = Thread(target=p2, args=('刘洋', lock1, lock2))
    t2.start()

- 递归锁解决死锁问题:把所有的锁变成一个递归锁
```python
import time
from threading import Thread, RLock, Lock


def p1(name, lock1, lock2):
    lock1.acquire()
    print('%s获得了汽车' % name)
    time.sleep(1)
    lock2.acquire()
    print('%s获得了摩托车' % name)
    lock1.release()
    print('%s归还了汽车' % name)
    lock2.release()
    print('%s归还了摩托车' % name)

def p2(name, lock1, lock2):
    lock2.acquire()
    print('%s获得了汽车' % name)
    time.sleep(1)
    lock1.acquire()
    print('%s获得了摩托车' % name)
    lock2.release()
    print('%s归还了汽车' % name)
    lock1.release()
    print('%s归还了摩托车' % name)

if __name__ == '__main__':
    lock1 = lock2 = RLock()
    # 把原来的两个互斥锁变为一个递归锁
    t1 = Thread(target=p1, args=('莉莉', lock1, lock2))
    t1.start()
    t2 = Thread(target=p2, args=('刘洋', lock1, lock2))
    t2.start()

    '''结果是:
    莉莉获得了汽车
    莉莉获得了摩托车
    莉莉归还了汽车
    莉莉归还了摩托车
    刘洋获得了汽车
    刘洋获得了摩托车
    刘洋归还了汽车
    刘洋归还了摩托车

    '''
```

线程之间出现死锁的问题本质在于资源分配顺序上的问题。在日常的开发中尽量使用互斥锁,因为互斥锁效率高,当然为了避免死锁的情况,需要添加上timeout=秒数,如果没有使用timeout在产品上线之后出现死锁问题了,那么在修改的时候也可以使用把所有的互斥锁改成同一个递归锁来解决死锁,这也是一种快速解决死锁的方式。

posted @ 2020-03-11 15:54  大道至诚  阅读(399)  评论(0编辑  收藏  举报