【问题标题】:Is the following code thread-safe in Python?以下代码在 Python 中是线程安全的吗?
【发布时间】:2018-05-23 22:07:55
【问题描述】:

我在 Python 模块中有类似于以下的代码:

_x = None

def set_x():
 global _x
 _x = # some value (will always be the same)

def worker():
 if _x is None:
  set_x()

在这个模块的客户端中创建的线程可以安全地调用worker()吗?

X 在模块的其他地方以只读模式使用;一旦设置它就永远不会改变。

是的,我意识到 x 可以封装在一个类中,并且线程可以在堆栈上创建自己的该类的实例,从而获得线程安全,但是为了这个问题的目的,假设基于类的实现是不可行或不可取的(例如,该模块表示一个单例,其中设置 x 的成本相对较高,我们希望尽可能不频繁地设置它)。

【问题讨论】:

  • 您知道x 仅在set_x 中被本地覆盖,并且将在全局范围内保持None
  • 是的!接得好。谢谢克劳斯。对于这个问题,假设每个引用 x 的函数在顶部都有必要的全局语句。

标签: python multithreading thread-safety


【解决方案1】:

假设您在set_x 中使用全局变量:

x = None

def set_x():
    global x
    x = "foo"

def worker():
    if not x:
        set_x()

if not x 和实际设置 x 之间存在竞争条件。所以多个线程可以为x赋值。这对于不可变对象几乎是无害的,但对于像列表这样的可变对象来说是有问题的,其中重新分配会丢失现有x 中的值。并且“大多数情况下”,即使是值比较的对象也可能具有不同的 id,因此 == 可能有效,但 is 可能会失败:

>>> a = "foo"
>>> b = "bar"
>>> c = a + b
>>> d = a + b
>>> c == d
True
>>> c is d
False

当然,不玩set_x 游戏的代码路径可能会在它们执行时发现xNone 更改为您的预期值。

特别是因为x 的计算成本很高,一个好的方法是将它隐藏在getter 后面。现在每个人都必须玩同样的游戏,只有一个线程会真正完成工作。

_x = None
_x_lock = threading.Lock()

def get_x():
    global _x
    if _x is None:
        with _x_lock:
            if _x is None:
                _x = "foobar"
    return _x

【讨论】:

  • 是的,x 是全局的——请查看更新后的示例。是的,设置 x 和测试它的值之间的竞争条件是我关心的问题。这个解决方案看起来非常健壮。假设 x 是一个字符串(在 Python IIRC 中是不可变的),我们是否仍处于原始代码的“大部分/总是?无害”领域?
  • 基本无害。如果它是一个硬编码的字符串,那么即使设置了多次,它也具有相同的 id。但是,如果它的构造,即使是相同的字符串也有不同的 id。所以,代码不能使用“is”(但无论如何也不应该)。 None 总是有相同的 id,所以 x is None 很好,但通常这是个坏主意。
  • 严格来说,x为None需要做两次测试,一个在获得锁之前,另一个在获得锁之后?
  • 您需要在锁内进行测试,以确保测试和更改不会被另一个线程中断。外部if 仅用于性能。它更快,一旦设置了x,就不需要抓住锁了。
  • 谢谢! (感谢您的答案中嵌入的许多其他 Python 提示;例如,使用前导下划线、x 为 None 与非 x 等)
猜你喜欢
  • 2010-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-25
  • 2011-05-18
相关资源
最近更新 更多