python死锁学习

举例

本文对Python死锁文章进行总结,对部分难以理解内容做了解释。
首先先列举一个会发生死锁的情况

死锁的一个原因是互斥锁。假设银行系统中,用户a试图转账100块给用户b,
与此同时用户b试图转账200块给用户a,则可能产生死锁。
2个线程互相等待对方的锁,互相占用着资源不释放。
#coding=utf-8
import time
import threading
class Account:
  def __init__(self, _id, balance, lock):
    self.id = _id
    self.balance = balance
    self.lock = lock

  def withdraw(self, amount):
    self.balance -= amount

  def deposit(self, amount):
    self.balance += amount


def transfer(_from, to, amount):
  if _from.lock.acquire():#锁住自己的账户
    _from.withdraw(amount)
    time.sleep(1)#让交易时间变长,2个交易线程时间上重叠,有足够时间来产生死锁
    print('wait for lock...')
    if to.lock.acquire():#锁住对方的账户
      to.deposit(amount)
      to.lock.release()
    _from.lock.release()
  print( 'finish...')

a = Account('a',1000, threading.Lock())
b = Account('b',1000, threading.Lock())
threading.Thread(target = transfer, args = (a, b, 100)).start()
threading.Thread(target = transfer, args = (b, a, 200)).start()

在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。举个例子:一个线程获取了第一个锁,然后在获取第二个锁的 时候发生阻塞,那么这个线程就可能阻塞其他线程的执行,从而导致整个程序假死。 解决死锁问题的一种方案是为程序中的每一个锁分配一个唯一的id,然后只允许按照升序规则来使用多个锁,这个规则使用上下文管理器 是非常容易实现的

先来看一下什么是上下文管理器@contextmanager
被装饰器装饰的函数分为三部分:

  1. with语句中的代码块执行前执行函数中yield之前代码
  2. yield返回的内容复制给as之后的变量
  3. with代码块执行完毕后执行函数中yield之后的代码

解决方案

import threading
from contextlib import contextmanager

# Thread-local state to stored information on locks already acquired
_local = threading.local()


@contextmanager
def acquire(*locks):
  # Sort locks by object identifier
  locks = sorted(locks, key=lambda x: id(x))

  # Make sure lock order of previously acquired locks is not violated
  acquired = getattr(_local, 'acquired', [])
  #如果出现新锁id大于旧锁id 报错
  if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
    raise RuntimeError('Lock Order Violation')
  # Acquire all of the locks
  acquired.extend(locks)
  _local.acquired = acquired
  try:
    for lock in locks:
      lock.acquire()
    yield		#无返回值 yield为空
  finally:
    # Release locks in reverse order of acquisition
    for lock in reversed(locks):
      lock.release()
    del _local.acquired[-len(locks):]

当然采用升序获取锁也存在一定的问题,看下列代码

import threading
x_lock = threading.Lock()
y_lock = threading.Lock()
 
def thread_1():
  while True:
        with acquire(x_lock):
            print("acquire x_lock")
            time.sleep(1)
            with acquire(y_lock):
                print('Thread-1')
                time.sleep(1)


def thread_2():
  while True:
        with acquire(y_lock):
            print('acquire y_lock')
            time.sleep(1)
            with acquire(x_lock):
                print('Thread-2')
 
t1 = threading.Thread(target=thread_1)
t1.daemon = False
t1.start()
 
t2 = threading.Thread(target=thread_2)
t2.daemon = False
t2.start()

daemon:
(1)如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,
如果该子线程还在运行,则主线程会等待它完成后再退出;
(2)如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检
查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论
是否运行完成。

代码运行结果为出现异常
在这里插入图片描述
发生崩溃的原因在于,每个线程都记录着自己已经获取到的锁。 acquire() 函数会检查之前已经获取的锁列表, 由于锁是按照升序排列获取的,所以函数会认为之前已获取的锁的id必定小于新申请到的锁,这时就会触发异常。
出现上述结果的原因是异常先出现在Thread-2,导致该线程中断。之后不会发生死锁现象了,因为只有一个线程Thread-1在运行,并且通过上下文管理器,每次内部acquire调用完毕后会自动释放锁,所以不会出现死锁现象了。

总结:

避免死锁是一种解决死锁问题的方式,在进程获取锁的时候会严格按照对象id升序排列获取,经过数学证明,这样保证程序不会进入 死锁状态。避免死锁的主要思想是,单纯地按照对象id递增的顺序加锁不会产生循环依赖,而循环依赖是 死锁的一个必要条件,从而避免程序进入死锁状态。

发布了16 篇原创文章 · 获赞 1 · 访问量 375

猜你喜欢

转载自blog.csdn.net/qq_41174940/article/details/104620663