Python中的线程-我缺少什么?
这是我第一次尝试Python线程。。。它失败得很惨:)我想实现一个基本的临界区问题,发现这段代码实际上没有问题 问题:为什么我对计数器增量没有问题?运行后计数器不应该有随机值吗?我只能在递增已经以原子方式执行,或者线程不是并发的情况下解释这一点Python中的线程-我缺少什么?,python,multithreading,Python,Multithreading,这是我第一次尝试Python线程。。。它失败得很惨:)我想实现一个基本的临界区问题,发现这段代码实际上没有问题 问题:为什么我对计数器增量没有问题?运行后计数器不应该有随机值吗?我只能在递增已经以原子方式执行,或者线程不是并发的情况下解释这一点 import threading import time turnstile_names = ["N", "E", "S", "W"] count = 0 class Counter(threading.Thread): def __init
import threading
import time
turnstile_names = ["N", "E", "S", "W"]
count = 0
class Counter(threading.Thread):
def __init__(self, id):
threading.Thread.__init__(self)
self.id = id
def run(self):
global count
for i in range(20):
#self.sem.acquire()
count = count + 1
#self.sem.release()
def main():
sem = threading.Semaphore(1)
counters = [Counter(name) for name in turnstile_names]
for counter in counters:
counter.start()
# We're running!
for counter in counters:
counter.join()
print count
return 0
if __name__ == '__main__':
main()
注意:我留下了acquire()
和release()
调用的注释以检查差异。我试图调整线程的速度,在增量后添加小的sleep
s,没有区别
解决方案/测试:谢谢Kevin(参见下面接受的答案)。我刚刚测试更改循环变量,得到了以下结果:
Loops Result
20 99% of the time 80. Sometimes 60.
200 99% of the time 800. Sometimes 600.
2000 Maybe 10% of the time different value
20000 Finally... random numbers! I've yet to see 80000 or 60000.
All numbers are now random, as originally expected.
我怀疑这似乎意味着线程开销是10^4增量操作的顺序
另一个有趣的测试(至少在我看来):
我在增量和found之后添加了time.sleep(random.random()/divisior)
,循环计数再次为20:
divisor result
100 always 4, so the race condition is always there.
1000 95% of the time 4, sometimes 3 or 5 (once 7)
10000 99% of the time NOT 4, varying from 4 to 13
100000 basically same as 10000
1000000 varying from 10 to 70
10000000... same as previous... (even with time.sleep(0))
如果增加每个线程的迭代次数:
def run(self):
global count
for i in range(100000):
#self.sem.acquire()
count = count + 1
#self.sem.release()
然后一个竞争条件就发生了。您的脚本打印了175165页,预计打印量为400000页。这表明递增不是原子的
增量不是原子的额外证据:CPython中线程的行为是由。根据维基的说法 全局解释器锁(GIL)是一个互斥锁,用于防止多个本机线程同时执行Python字节码 如果GIL具有字节码级别的粒度,那么我们希望增量不是原子的,因为它需要多个字节码来执行,如
dis
模块所示:
>>> import dis
>>> def f():
... x = 0
... x = x + 1
...
>>> dis.dis(f)
2 0 LOAD_CONST 1 (0)
3 STORE_FAST 0 (x)
3 6 LOAD_FAST 0 (x)
9 LOAD_CONST 2 (1)
12 BINARY_ADD
13 STORE_FAST 0 (x)
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
这里,递增动作由字节码6到13执行
那么,为什么原始代码没有显示竞争条件呢?这似乎是因为每个线程的预期寿命很短——只要循环20次,每个线程就会在下一个线程开始自己的工作之前完成它的工作并死亡。在Cpython中,线程的安全性取决于原子性(一个字节码不会被中断)、GIL(python锁定一个线程大约100个“滴答”)祝你好运。反编译一个更简单的函数
>>> import dis
>>> count = 0
>>> def x():
... count = count + 1
...
>>> dis.dis(x)
2 0 LOAD_FAST 0 (count)
3 LOAD_CONST 1 (1)
6 BINARY_ADD
7 STORE_FAST 0 (count)
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
我们看到代码可以在加载和存储之间中断。这可能意味着一个线程加载一个值,并被挂起,最终会用其结果覆盖一个更大的值
现在运气来了。做20次手术并不多。让我们更改代码,以将计数作为参数,并查看较大值的情况
import threading
import time
import sys
turnstile_names = ["N", "E", "S", "W"]
count = 0
class Counter(threading.Thread):
def __init__(self, id):
threading.Thread.__init__(self)
self.id = id
def run(self):
global count
for i in range(int(sys.argv[1])):
#self.sem.acquire()
count = count + 1
#self.sem.release()
def main():
sem = threading.Semaphore(1)
counters = [Counter(name) for name in turnstile_names]
for counter in counters:
counter.start()
# We're running!
for counter in counters:
counter.join()
print count
return 0
if __name__ == '__main__':
main()
我跑了一次:
td@timsworld2:~/tmp/so$ python count.py 1
4
td@timsworld2:~/tmp/so$ python count.py 2
8
td@timsworld2:~/tmp/so$ python count.py 20
80
td@timsworld2:~/tmp/so$ python count.py 200
749
td@timsworld2:~/tmp/so$ python count.py 2000
4314
4个线程中有2000个线程,我丢失了将近一半的值。预期和实际输出是什么?感谢您的快速回复!那么,python中线程的粒度在语句级?语句总是以原子方式执行?好的,语句具有字节码级别的粒度。而且
x=x+1
编译了大约四段字节码,所以我不希望它是原子的。我无法解释为什么这里会出现其他情况。Nvm,我得到了竞争条件。您是否尝试增加迭代次数?20不是那么多,你可能不会在有问题的地方碰到一个中断,就是这样!看起来,在线程处理的剩余时间里,实际增量在统计上丢失了。当你回答的时候,我正在做同样的测试(原始问题编辑)。看起来@Kevin做了同样的事情,而且是一个更快的打字员!