【Python笔记】Python线程中的“锁机制”


1 Lock( 锁 )

1.1 何为Lock( 锁 )?

一个简单的例子:

有一个奇葩的房东,他家里有两个房间想要出租。
这个房东很抠门,家里有两个房间,但却只有一把锁,不想另外花钱是去买另一把锁,也不让租客自己加锁。这样租客只有,先租到的那个人才能分配到锁。

X先生,率先租到了房子,并且拿到了锁。而后来者Y先生,由于锁已经已经被X取走了,自己拿不到锁,也不能自己加锁,Y就不愿意了。也就不租了,换作其他人也一样,没有人会租第二个房间,直到X先生退租,把锁还给房东,可以让其他房客来取。第二间房间才能租出去。

换句话说,就是房东同时只能出租一个房间,一但有人租了一个房间,拿走了唯一的锁,就没有人再在租另一间房了。

回到我们的线程中来,有两个线程A和B,A和B里的程序都加了同一个锁对象,当线程A率先执行到lock.acquire()(拿到全局唯一的锁后),线程B只能等到线程A释放锁lock.acquire()(拿到全局唯一的锁)才能运行并执行后面的代码。

1.2 如何使用Lock( 锁 )?

Python的线程操作在旧版本中使用的是thread模块,在Python27和Python3中引入了threading模块,同时thread模块在Python3中改名为_thread模块,threading模块相较于thread模块,对于线程的操作更加的丰富,而且threading模块本身也是相当于对thread模块的进一步封装而成,thread模块有的功能threading模块也都有,所以涉及到多线程的操作,推荐使用threading模块

threading模块中包含了关于线程操作的丰富功能,包括:常用线程函数,线程对象,锁对象,递归锁对象,事件对象,条件变量对象,信号量对象,定时器对象,栅栏对象。

注:本文使用的Python版本是Python 3.7.4

简单看下代码,学习如何加锁,获取钥匙,释放锁。

import threading

# 生成锁对象,全局唯一
lock=threading.Lock()

# 获取锁。未获取到会阻塞程序,直到获取到锁才会往下执行
lock.acquire()

# 释放锁,归回房东,其他人可以拿去用了
lock.release()

需要注意的是lock.acquire() 和 lock.release()必须成对出现。否则就有可能造成死锁

很多时候,我们虽然知道,他们必须成对出现,但是还是难免会有忘记的时候。

为了,规避这个问题,推荐使用上下文管理器来加锁。

with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。

import threading

lock=threading.Lock()
with lock:
    # 此处添加自己的代码
    pass

1.3 为何要使用Lock( 锁 )?

你现在肯定还是一脸懵逼,这么麻烦,我不用锁不行吗?有的时候还真不行。

那么为了说明锁存在的意义。我们分别来看下,不用锁的情形有怎样的问题。

定义两个函数,分别在两个线程中执行。这两个函数 共用 一个变量 n

import threading

def job1():
    global n
    for i in range(10):
        n+=1
        print('job1',n)
def job2():
    global n
    for i in range(10):
        n+=100
        print('job2',n)


n=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()

job1 1
job1 2
job1 3
job1 4job2 
job1 105
job1104
job2 106
job1  206
207
job1 job2 208
job1 308
job2309 409

job2 509
job1 510
job2 610
job2 710
job2 810
job2 910
job2 1010

结果是不是很乱?完全不是预想的那样。

解释下这是为什么?因为两个线程共用一个全局变量,又由于两线程是交替执行的,当job1 执行三次 +1 操作时,job2就不管三七二十一 给n做了+10操作。两个线程之间,执行完全没有规矩,没有约束。所以会看到输出当然也很乱。

加了锁后,这个问题也就解决,来看看。

import threading

def job1():
    global n,lock
    lock.acquire()
    for i in range(10):
        n+=1
        print('job1',n)
    lock.release()
def job2():
    global n,lock
    # 因为只有2个job,job1执行完了job2这里加不加lock不影响结果
    lock.acquire() 
    for i in range(10):
        n+=100
        print('job2',n)
    lock.release()

if __name__=='__main__':
    n=0
    # 生成锁对象
    lock=threading.Lock()

    t1=threading.Thread(target=job1)
    t2=threading.Thread(target=job2)
    t1.start()
    t2.start()
job1 1
job1 2
job1 3
job1 4
job1 5
job1 6
job1 7
job1 8
job1 9
job1 10
job2 110
job2 210
job2 310
job2 410
job2 510
job2 610
job2 710
job2 810
job2 910
job2 1010

由于job1的线程,率先拿到了锁,所以在for循环中,没有人有权限对n进行操作。当job1执行完毕释放锁后,job2这才拿到了锁,开始自己的for循环。

这里,你应该也知道了,加锁是为了对锁内资源(变量)进行锁定,避免其他线程篡改已被锁定的资源,以达到我们预期的效果。

为了避免大家忘记释放锁,后面的例子,我将都使用with上下文管理器来加锁。

1.4 可重入锁(RLock)

有时候在同一个线程中,我们可能会多次请求同一资源(就是,获取同一锁钥匙),俗称锁嵌套

如果还是按照常规的做法,会造成死锁的。比如,下面这段代码,你可以试着运行一下,会发现并没有输出结果

是因为,第二次获取锁时,发现锁已经被同一线程的人拿走了。自己也就理所当然,拿不到锁,程序就卡住了。

import threading

def main():
    n=0
    lock=threading.Lock()
    with lock:
        for i in range(10):
            n+=1
            with lock:
                print(n)

t1=threading.Thread(target=main)
t1.start()

那么如何解决这个问题呢。

threading模块除了提供Lock锁之外,还提供了一种可重入锁RLock,专门来处理这个问题。

import threading

def main():
    n=0
    # 生成可重入锁对象
    lock=threading.RLock()
    with lock:
        for i in range(10):
            n+=1
            with lock:
                print(n)

t1=threading.Thread(target=main)
t1.start()
1
2
3
4
5
6
7
8
9
10

需要注意的是,可重入锁,只在同一线程里,放松对锁钥匙的获取,其他与Lock并无二致

1.5 防止死锁的加锁机制

在编写多线程程序时,可能无意中就会写了一个死锁。可以说,死锁的形式有多种多样,但是本质都是相同的,都是对资源不合理竞争的结果。

以本人的经验总结,死锁通常以下几种:

  • 同一线程,嵌套获取同把锁,造成死锁(可重入锁RLock解决)
  • 多个线程,不按顺序同时获取多个锁。造成死锁

主要是第二种。可能你还没明白,是如何死锁的。举个例子:

线程1,嵌套获取A,B两个锁,线程2,嵌套获取B,A两个锁。

由于两个线程是交替执行的,是有机会遇到线程1获取到锁A,而未获取到锁B,在同一时刻,线程2获取到锁B,而未获取到锁A。由于锁B已经被线程2获取了,所以线程1就卡在了获取锁B处,由于是嵌套锁,线程1未获取并释放B,是不能释放锁A的,这是导致线程2也获取不到锁A,也卡住了。两个线程,各执一锁,各不让步。造成死锁。

经过数学证明,只要两个(或多个)线程获取嵌套锁时,按照固定顺序就能保证程序不会进入死锁状态。那么问题就转化成如何保证这些锁是按顺序的?

两个办法:

  • 人工自觉
  • 人工识别

第一种,就不说了。

第二种,写一个辅助函数来对锁进行排序。可以参考如下代码。

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',[])
    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
    finally:
        # Release locks in reverse order of acquisition
        for lock in reversed(locks):
            lock.release()
        del acquired[-len(locks):]
x_lock=threading.Lock()
y_lock=threading.Lock()
def thread1():
    while True:
        with acquire(x_lock):
            with acquire(y_lock):
                print('Thread-1')
def thread2():
    while True:
        with acquire(y_lock):
            with acquire(x_lock):
                print('Thread-2')

if __name__=='__main__':
    t1=threading.Thread(target=thread1)
    t1.daemon=True
    t1.start()

    t2=threading.Thread(target=thread2)
    t2.daemon=True
    t2.start()
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1

利用contextlib和@contextmanager实现with语句上下文实例

Python标准模块–ContextManager

import hashlib
from contextlib import contextmanager
#contextlib
'''
任何对象,只要正确实现了上下文管理,就可以用于with语句。
实现上下文管理是通过__enter__和__exit__这两个方法实现的,
也可以通过@contextmanager和closing函数实现
'''
print
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值