laixintao / python-parallel-programming-cookbook-cn

📖《Python Parallel Programming Cookbook》中文版
http://python-parallel-programmning-cookbook.readthedocs.io/
1.49k stars 93 forks source link

疑问: Chapter2.关于线程没加锁导致的条件竞争 #47

Closed jerrychan807 closed 5 years ago

jerrychan807 commented 5 years ago

遇到的情况:

Run了书上Chapter2的Thread synchronization with lock and RLock的第一个例子

发现和书上的结果不一样.

预期是如果两个thread没加lock,去对shared memory操作时,会出现条件竞争.

代码:

# -*- coding:utf-8 -*-

# @Time    : 2018/12/15 4:44 PM
# @Author  : jerry

import threading

shared_resource_with_lock = 0
shared_resource_with_no_lock = 0
COUNT = 100000
shared_resource_lock = threading.Lock()

#### NO LOCK MANAGEMENT
def increment_without_lock():
    global shared_resource_with_no_lock
    for i in range(COUNT):
        shared_resource_with_no_lock += 1

def decrement_without_lock():
    global shared_resource_with_no_lock
    for i in range(COUNT):
        shared_resource_with_no_lock -= 1

if __name__ == '__main__':
    t3 = threading.Thread(target=increment_without_lock)
    t4 = threading.Thread(target=decrement_without_lock)

    t3.start()
    t4.start()

    t3.join()
    t4.join()

    print("the value of shared variable with race condition is %s" % shared_resource_with_no_lock)

运行结果:

在我的电脑上运行,当COUNT为100000时,运行几次后,结果都是一样的。 the value of shared variable with race condition is 0.

把COUNT变大,设置为1000000 才能出现预期的结果. the value of shared variable with race condition is 1382.

我的看法:

 t3.start() #  t3 开始执行
 t4.start() #  t4 开始执行

 t3.join() # 确保t3 结束
 t4.join() # 确保t4 结束

t3t4不是同时start的 当Count比较小等于的时候,t3在t4.start()之前已经结束了,把全局的shared_resource_with_no_lock加到100000了,

这个时候t4.start()了,把全局的shared_resource_with_no_lock减回到0了,

最终输出结果是 the value of shared variable with race condition is 0. 此时并没有出现预期的条件竞争.

只有当Count足够大,以至于,t3在t4.start()之前,这个很短的时间内不能run完,才会出现预期的条件竞争,导致最终的输出结果不为0

看来这个跟运行机器的性能有关系呀~

不知道我的理解是否正确? please help~

laixintao commented 5 years ago

你的环境是什么? 我怀疑 Python3 的表现和 Python2 可能不一样。

以下,Python2 每次都会出现竞争条件,Python3 偶尔会出现:

➜  tmp python2.7 test.py
the value of shared variable with lock management is 0
the value of shared variable with race condition is 282
➜  tmp python2.7 test.py
the value of shared variable with lock management is 0
the value of shared variable with race condition is 1021
➜  tmp python2.7 test.py
the value of shared variable with lock management is 0
the value of shared variable with race condition is -4656
➜  tmp python3 test.py
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
➜  tmp python3 test.py
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
➜  tmp python3 test.py
the value of shared variable with lock management is 0
the value of shared variable with race condition is -56485
jerrychan807 commented 5 years ago

py2

MacBook-Pro:p42 chanjerry$ python thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 602
MacBook-Pro:p42 chanjerry$ python thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 955
MacBook-Pro:p42 chanjerry$ python thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is -4030
MacBook-Pro:p42 chanjerry$ python thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 43
MacBook-Pro:p42 chanjerry$ python thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is -222
MacBook-Pro:p42 chanjerry$ python thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is -430
MacBook-Pro:p42 chanjerry$ python thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is -4503

py3

MacBook-Pro:p42 chanjerry$ python3 thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
MacBook-Pro:p42 chanjerry$ python3 thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
MacBook-Pro:p42 chanjerry$ python3 thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
MacBook-Pro:p42 chanjerry$ python3 thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
MacBook-Pro:p42 chanjerry$ python3 thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
MacBook-Pro:p42 chanjerry$ python3 thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
MacBook-Pro:p42 chanjerry$ python3 thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0
MacBook-Pro:p42 chanjerry$ python3 thread_lock.py 
the value of shared variable with lock management is 0
the value of shared variable with race condition is 0

额,我这边python2也是每次都会出现竞争条件,Python3 不会出现,运行20+次以上,都不会出现条件竞争。

当把COUNT变大,设为100000时,py2和py3都会出现条件竞争.🙂

jerrychan807 commented 5 years ago
# -*- coding:utf-8 -*-

# @Time    : 2018/12/15 4:44 PM
# @Author  : jerry

import threading

shared_resource_with_no_lock = 0
COUNT = 100000
import time

#### NO LOCK MANAGEMENT
def increment_without_lock():
    start = time.time()
    global shared_resource_with_no_lock
    for i in range(COUNT):
        shared_resource_with_no_lock += 1
    end = time.time()
    print("increment:{0}".format(str(end - start)))
    print('\n')

def decrement_without_lock():
    start = time.time()
    global shared_resource_with_no_lock
    for i in range(COUNT):
        shared_resource_with_no_lock -= 1
    end = time.time()
    print("decrement:{0}".format(str(end - start)))

if __name__ == '__main__':
    t3 = threading.Thread(name='t3', target=increment_without_lock)
    t4 = threading.Thread(name='t4', target=decrement_without_lock)

    t3.start()
    t4.start()

    t3.join()
    t4.join()

    print("the value of shared variable with race condition is %s" % shared_resource_with_no_lock)

py2:

$ python2 thread_without_lock.py 
decrement func use time:0.0448379516602
increment func use time:0.0543751716614
the value of shared variable with race condition is -6703

py3

$python3 thread_without_lock.py 
increment func use time:0.00865316390991211
decrement func use time:0.00800633430480957
the value of shared variable with race condition is 0
 t3.start() #  t3 开始执行
 t4.start() #  t4 开始执行

 t3.join() # 确保t3 结束
 t4.join() # 确保t4 结束

py3运行速度更快?!🙂 在py3中,Count=100000,比较小时, t4.start()来不及参与use shared memory的时候,t3任务结束了。

laixintao commented 5 years ago

py3运行速度更快?!🙂

这个答案是肯定的,Python3 比 Python2 快。

Python3 的 GIL 机制效率更高一些,看下这里:https://www.reddit.com/r/Python/comments/4vyg2m/threadinglocking_is_4x_as_fast_on_python_3_vs/

至于为什么 Python3 出现竞争条件需要的时间更长,这个我也不太清楚。回去研究一下,要是搞明白的给你分享一下 :) 我猜可能也和 GIL 的新机制有关吧。

jerrychan807 commented 5 years ago

好呀,我有空也去研究下 谢谢啦~🙂

laixintao commented 5 years ago

上一条可能有误导性,我更新了一下。我觉得可能有两种原因:

  1. Python3 运行速度太快了,第二个线程还没开始,第一个线程就跑完了(不过为什么这段简单的代码会快这么多?)
  2. Python3 的 GIL 机制和 Python2 不同,切换的时间长了一些,导致同样的代码不容易出现竞争机制。
laike9m commented 5 years ago

在我电脑上测试了一下,确实一旦 increment_without_lock 在 join 之前结束,最后结果就是 0;如果没结束,就不是。

讨论 GIL 实现机制我觉得意义不大,因为开发者不该关心 or rely on 这个。只要知道加锁避免就好了。

laixintao commented 5 years ago

Hi Sorry for the late.

答案不会是 Python3 运行速度问题,而是 Python3 的 GIL 切换机制导致竞争条件出现的几率更小了(但是依然会出现),所以对于开发者来说没有什么用,该加锁的依然要枷锁。只不过平时测试测到竞争条件的几率更小了。

Raymond 这个视频很赞,里面讲到了 Python3 更难复现竞争条件: https://youtu.be/9zinZmE3Ogk?t=2487 以及提到了一个手动插入 FUZZY 的方法来测试。

新年快乐!