python线程安全的计数器_+ =运算符在Python中是线程安全的吗?

+ =运算符在Python中是线程安全的吗?

我想为实验创建一个非线程安全的代码块,这些是2个线程将要调用的函数。

c = 0

def increment():

c += 1

def decrement():

c -= 1

此代码线程安全吗?

如果不是,我可以理解为什么它不是线程安全的,以及哪种类型的语句通常会导致非线程安全的操作。

如果它是线程安全的,如何使它显式为非线程安全的?

8个解决方案

88 votes

不,该代码绝对是绝对线程安全的。

import threading

i = 0

def test():

global i

for x in range(100000):

i += 1

threads = [threading.Thread(target=test) for t in range(10)]

for t in threads:

t.start()

for t in threads:

t.join()

assert i == 1000000, i

持续失败。

i + = 1解析为四个操作码:加载i,加载1,将两者相加,然后将其存储回i。 Python解释器每100个操作码切换一次活动线程(通过从一个线程释放GIL,以便另一个线程可以拥有它)。 (这两个都是实现细节。)当在加载和存储之间发生100操作码抢占时,就会发生竞争状态,从而允许另一个线程开始递增计数器。 当它返回到挂起的线程时,它将继续使用旧值“ i”,并且同时撤消其他线程运行的增量。

使它成为线程安全的很简单。 添加锁:

#!/usr/bin/python

import threading

i = 0

i_lock = threading.Lock()

def test():

global i

i_lock.acquire()

try:

for x in range(100000):

i += 1

finally:

i_lock.release()

threads = [threading.Thread(target=test) for t in range(10)]

for t in threads:

t.start()

for t in threads:

t.join()

assert i == 1000000, i

Glenn Maynard answered 2020-07-27T15:56:46Z

28 votes

(注意:每个函数都需要inc才能使代码正常工作。)

此代码线程安全吗?

否。在CPython中,只有一个字节码指令是“原子的”,即使所涉及的值是简单的整数,inc也可能不会产生单个操作码:

>>> c= 0

>>> def inc():

... global c

... c+= 1

>>> import dis

>>> dis.dis(inc)

3 0 LOAD_GLOBAL 0 (c)

3 LOAD_CONST 1 (1)

6 INPLACE_ADD

7 STORE_GLOBAL 0 (c)

10 LOAD_CONST 0 (None)

13 RETURN_VALUE

因此,一个线程可以在装入c和1的情况下到达索引6,放弃GIL并让另一个线程进入,该线程执行inc并进入睡眠状态,将GIL返回到第一个线程,该线程现在具有错误的值。

无论如何,原子性是您不应该依赖的实现细节。 字节码在将来的CPython版本中可能会更改,并且在不依赖GIL的其他Python实现中,结果将完全不同。 如果需要线程安全,则需要锁定机制。

bobince answered 2020-07-27T15:57:24Z

15 votes

确保我建议使用锁:

import threading

class ThreadSafeCounter():

def __init__(self):

self.lock = threading.Lock()

self.counter=0

def increment(self):

with self.lock:

self.counter+=1

def decrement(self):

with self.lock:

self.counter-=1

同步装饰器还可以帮助使代码易于阅读。

gillesv answered 2020-07-27T15:57:48Z

10 votes

很容易证明您的代码不是线程安全的。 您可以通过在关键部分使用睡眠来提高查看比赛状况的可能性(这只是模拟CPU速度慢)。 但是,如果您将代码运行足够长的时间,无论如何最终都应该看到竞争状态。

from time import sleep

c = 0

def increment():

global c

c_ = c

sleep(0.1)

c = c_ + 1

def decrement():

global c

c_ = c

sleep(0.1)

c = c_ - 1

John La Rooy answered 2020-07-27T15:58:10Z

4 votes

简短的回答:不。

长答案:一般不会。

尽管CPython的GIL使单个操作码具有线程安全性,但这不是一般行为。 您可能不会假设,即使简单的操作(例如加法)也是原子指令。 当另一个线程运行时,添加可能仅完成一半。

而且,一旦函数在多个操作码中访问一个变量,线程安全性就会消失。 如果将函数体包装在锁中,则可以生成线程安全性。 但是请注意,锁可能会在计算上耗费大量资源并可能产生死锁。

ebo answered 2020-07-27T15:58:44Z

3 votes

单一操作码由于具有GIL而具有线程安全性,但除此之外:

import time

class something(object):

def __init__(self,c):

self.c=c

def inc(self):

new = self.c+1

# if the thread is interrupted by another inc() call its result is wrong

time.sleep(0.001) # sleep makes the os continue another thread

self.c = new

x = something(0)

import threading

for _ in range(10000):

threading.Thread(target=x.inc).start()

print x.c # ~900 here, instead of 10000

多个线程共享的每个资源都必须有一个锁。

Jochen Ritzel answered 2020-07-27T15:56:12Z

2 votes

如果您实际上想使代码不是线程安全的,并且很有可能在不尝试一万次的情况下(或在您真正不希望发生“坏的”事情发生的情况下)就发生“坏的”事情, 您可以通过显式睡眠来“抖动”您的代码:

def íncrement():

global c

x = c

from time import sleep

sleep(0.1)

c = x + 1

Rasmus Kaj answered 2020-07-27T15:59:04Z

0 votes

您确定函数递增和递减执行没有任何错误吗?

我认为它应该引发UnboundLocalError,因为您必须明确地告诉Python您要使用名为'c'的全局变量。

因此,将递增(也递减)更改为以下内容:

def increment():

global c

c += 1

我认为您的代码也是线程不安全的。 有关Python中的线程同步机制的本文可能会有所帮助。

ardsrk answered 2020-07-27T15:59:37Z

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值