[python] 线程间同步之Lock RLock

为什么需要同步

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

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并发编程
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 1.进程和线程 队列:1、进程之间的通信: q = multiprocessing.Queue()2、...
    一只写程序的猿阅读 1,098评论 0 17
  • 线程 1.同步概念 1.多线程开发可能遇到的问题 同步不是一起的意思,是协同步调 假设两个线程t1和t2都要对nu...
    TENG书阅读 602评论 0 1
  • 线程状态新建,就绪,运行,阻塞,死亡。 线程同步多线程可以同时运行多个任务,线程需要共享数据的时候,可能出现数据不...
    KevinCool阅读 793评论 0 0
  • 概述 多线程给我们带来的好处是可以并发的执行多个任务,特别是对于I/O密集型的业务,使用多线程,可以带来成倍的性能...
    SimonChen阅读 9,430评论 0 5
  • 我一直在反思自己的教育方式,不知道问题出现在哪里。看了其他家长的日记,我觉得我太对不起孩子了!或许是职业病,或...
    爱生活的Rex阅读 137评论 0 1