美文网首页
[python] 线程间同步之Lock RLock

[python] 线程间同步之Lock RLock

作者: StormZhu | 来源:发表于2018-05-02 22:59 被阅读0次

为什么需要同步

同样举之前的例子,两个线程分别对同一个全局变量进行加减,得不到预期结果,代码如下:

total = 0
def add():
    global total
    for i in range(1000000):
        total += 1
def desc():
    global total
    for i in range(1000000):
        total -= 1
import threading
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)

原因就是因为 +=-=并不是原子操作。可以使用dis模块查看字节码:

import dis
def add(total):
    total += 1
def desc(total):
    total -= 1
total = 0
print(dis.dis(add))
print(dis.dis(desc))

# 运行结果如下:
#   3           0 LOAD_FAST                0 (total)
#               3 LOAD_CONST               1 (1)
#               6 INPLACE_ADD
#               7 STORE_FAST               0 (total)
#              10 LOAD_CONST               0 (None)
#              13 RETURN_VALUE
# None
#   5           0 LOAD_FAST                0 (total)
#               3 LOAD_CONST               1 (1)
#               6 INPLACE_SUBTRACT
#               7 STORE_FAST               0 (total)
#              10 LOAD_CONST               0 (None)
#              13 RETURN_VALUE
# None

可以看到 add()函数虽然其中只有一行代码,但是字节码主要分为四个步骤:

  1. load 变量total
  2. load 常量 1
  3. 执行加法操作
  4. 对total进行赋值

同理,desc()函数的步骤相同,只是第三步改为执行减法。

假设一种极端情况,开始total = 0,首先线程1 load 变量total,得到值为0,切换到线程2,同样的到total为0,再次切换线程1 load常量1,执行加法,给total赋值得到1;然后线程2也 laod常量1,执行减法,给total赋值为-1。最终total为-1,而不是预期的0

期望中,必须在+=操作结束后,才能执行-=,所以线程同步的需求就出来了。

互斥锁Lock

threading模块中提供了threading.Lock类(互斥锁),基本用法如下:

import threading
lock = threading.Lock()
lock.acquire() # 获取锁
# dosomething…… # 临界区的代码只能被同时只能被一个线程运行
lock.release() # 释放锁

将上面的代码修改,即可得到正确结果:

import threading
total = 0
lock = threading.Lock()
def add(lock):
    global total
    for i in range(1000000):
        lock.acquire()
        total += 1
        lock.release()
def desc(lock):
    global total
    for i in range(1000000):
        lock.acquire()
        total -= 1
        lock.release()

thread1 = threading.Thread(target=add, args=(lock,))
thread2 = threading.Thread(target=desc, args=(lock,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)

# 执行结果 0

可以看到,添加互斥锁之后,程序执行结果是正确的,但是用了互斥锁之后,同样有一些缺陷:

  • 添加锁之后,会影响程序性能。
  • 可能引起"死锁"。

死锁主要在两种情况下发生:

  1. 迭代死锁

    一个线程“迭代”请求同一个资源 ,会造成死锁。

    lock = threading.Lock()
    lock.acquire()
    lock.acquire()
    total += 1
    lock.release()
    lock.release()
    

    上例种,第一次请求资源后还未 release ,再次acquire,最终无法释放,造成死锁 。(可通过可重入锁解决这个问题)。

  2. 互相调用死锁

    两个线程中都会调用相同的资源,互相等待对方结束的情况 。假设A线程需要资源a,b,B线程也需要资源a,b,而线程A先获取资源a后,再获取资源b, B线程先获取资源b,再获取资源a。在A线程获取资源a,B线程获取资源b后,A线程在等待B线程释放资源b,而B线程在等待A线程释放资源a,从而死锁就发生了

    import threading
    import time
    lock_a = threading.Lock()
    lock_b = threading.Lock()
    
    def func1():
        global lock_a
        global lock_b
    
        lock_a.acquire()
        time.sleep(1)
        lock_b.acquire()
        time.sleep(1)
        lock_b.release()
        lock_a.release()
        
    def func2():
        global lock_a
        global lock_b
    
        lock_b.acquire()
        time.sleep(1)
        lock_a.acquire()
        time.sleep(1)
        lock_a.release()
        lock_b.release()
    
    thread1 = threading.Thread(target=func1)
    thread2 = threading.Thread(target=func2)
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()
    print("program finished")
    # 程序会陷入死循环
    

    这个例子比较重要,开始理解错了,如果B线程获取了资源b,然后释放之后再获取资源a,这样是不会发生死锁的。只有在B线程获取了资源b,还没有释放的时候,获取了资源a,才会发生死锁。

可重入锁RLock

为解决同一线程种不能多次请求同一资源的问题,python提供了“可重入锁”:threading.RLockRLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源 。用法和threading.Lock类相同。

将上面迭代死锁的代码改写一下,就不会发生死锁,但注意,调用acquirerelease的次数必须相等。

lock = threading.RLock()
lock.acquire()
lock.acquire()
total += 1
lock.release()
lock.release()

一般不会写这么无聊的代码,但是有一种情况是可能发生的,在加锁区域调用了某个函数,而这个函数内部又申请了同样的资源。

lock = threading.RLock()
def dosomething(lock):
    lock.acquire()
    # do something
    lock.release()
    
lock.acquire()
dosomething(lock)
lock.release()

总结

  • 线程间访问同一变量需要同步。
  • 线程间加锁会导致性能损失。
  • 加锁可能产生死锁,迭代死锁互相调用死锁
  • 可重入锁可以避免迭代死锁。

参考

  1. 举例讲解 Python 中的死锁、可重入锁和互斥锁
  2. Python3高级编程和异步IO并发编程

相关文章

  • [python] 线程间同步之Lock RLock

    为什么需要同步 同样举之前的例子,两个线程分别对同一个全局变量进行加减,得不到预期结果,代码如下: 原因就是因为 ...

  • 并发 :线程间同步、锁、可重入锁及互斥锁

    线程间同步 线程间同步涉及线程互斥锁; 锁(Lock)容易导致死锁,可重入锁(RLock)则不会导致死锁,但每次 ...

  • python3线程同步,Lock、Rlock、Condition

    线程同步 Lock、Rlock锁机制 使用锁的原因 为了避免线程间进行数据竞争,有时必须使用一些机制来强制线程同步...

  • 2019-11-26 python多线程基础

    看文档发现Python是借鉴Java的多线程,学学java还是有用的。 Lock和RLock的区别 RLock 叫...

  • 线程同步

    使用Thread对象的Lock和RLock可以实现简单的线程同步,这两个对象都有acquire方法和release...

  • 2022-03-21

    占个坑python lock rlock 锁详解 用例

  • Python 3 多线程编程

    本文主要基于python 3.5实现多线程编程 1. 创建多线程 2. 多线程间的同步,lock机制 3. que...

  • python多线程锁Lock和RLock

    如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步,使用 Th...

  • Python 递归锁

    其中RLock是递归锁,而Lock是互斥锁。其区别是Rlock实例化之后该对象可以在一个线程一直去acquire(...

  • python深入系列(三):python并行编程

    1、Lock和RLock Lock:基本锁,只能加一次,加锁之后其余锁请求处于锁释放的等待中RLock:Reent...

网友评论

      本文标题:[python] 线程间同步之Lock RLock

      本文链接:https://www.haomeiwen.com/subject/cfuhrftx.html