【问题标题】:Why reversed() removes thread safety?为什么 reversed() 会删除线程安全?
【发布时间】:2021-02-02 03:57:11
【问题描述】:

CPython 中用于确保迭代线程安全的一个常见习惯用法是使用tuple()

例如 - tuple(dict.items()) 在 CPython 中保证是线程安全的,即使项目被不同的线程删除。

这是因为解释器在运行这些 C 函数时不运行 eval 循环并且不释放 GIL。我已经测试过了,效果很好。

但是,tuple(reversed(dict.items())) 似乎不是线程安全的,我不明白为什么。它没有运行任何 Python 函数,也没有显式释放 GIL。如果在 dict 在不同的线程上运行时删除密钥,为什么我仍然会收到错误消息?

【问题讨论】:

  • reversed 处理 dict 和相关的 dict 视图相对较新,请参阅 herehere 也许这是一个实际的错误。
  • @juanpa.arrivillaga 我不确定错误是否是正确的术语,因为 GIL 本身并没有给你这种保证,而且你在技术上应该使用锁。然而这是出乎意料的,我想知道为什么它会这样工作。
  • 也许是监督
  • 当然,如果您需要此类数据结构上的线程安全,您应该使用显式锁,即使在给定的 Python 版本中您可以摆脱它。请记住,线程并发的这种不可预测性是支持现代单线程异步的最佳论据之一。即使对于“元组”构造函数,这种“线程安全”也应该被视为实现细节。
  • @jsbueno ofc 它应该算作一个实现细节,但又为什么 gil 会被释放?

标签: python thread-safety cpython python-internals gil


【解决方案1】:

在迭代 dict 时修改它的大小总是会出错。 tuple(d.items()) 之所以是线程安全的,是因为迭代器的检索和迭代都发生在同一个 C 函数中。 d.items() 创建了一个 dict_items 对象,但还没有迭代器。这就是字典大小的变化仍然反映的原因:

>>> d = {'a': 1, 'b': 2}
>>> view = d.items()
>>> del d['a']
>>> list(view)
[('b', 2)]

然而,一旦迭代器被检索到,字典大小must not change 就不再存在了:

>>> d = {'a': 1, 'b': 2}
>>> iterator = iter(d.items())
>>> del d['a']
>>> list(iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

这就是使用reversed 时发生的情况:它creates a reverse iterator 的dict 项。造成麻烦的是迭代器部分,因为一旦创建了迭代器,底层字典的大小就不能改变:

>>> d = {'a': 1, 'b': 2}
>>> r = reversed(d.items())
>>> r  # Note the iterator here.
<dict_reverseitemiterator object at 0x7fb3a4aa24f0>
>>> del d['a']
>>> list(r)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

因此,在特定示例中tuple(reversed(dict.items())) 创建了原始字典的迭代器reversed(dict.items()),然后由tuple 迭代。但是,此迭代器要求 dict 的大小不发生变化。就像tuple(iter(dict.items())) 只是倒序排列。

关于 GIL 开关,eval 循环在它获取 reversed() 的结果时运行,在创建迭代器之后,并将其发送到 tuple() 进行迭代。请参阅以下反汇编:

>>> dis.dis("tuple({}.items())")
  1           0 LOAD_NAME                0 (tuple)
              2 BUILD_MAP                0
              4 LOAD_METHOD              1 (items)
              6 CALL_METHOD              0
              8 CALL_FUNCTION            1
             10 RETURN_VALUE
>>> dis.dis("tuple(reversed({}.items()))")
  1           0 LOAD_NAME                0 (tuple)
              2 LOAD_NAME                1 (reversed)
              4 BUILD_MAP                0
              6 LOAD_METHOD              2 (items)
              8 CALL_METHOD              0
             10 CALL_FUNCTION            1
             12 CALL_FUNCTION            1
             14 RETURN_VALUE

【讨论】:

  • 太棒了,所以最后一点是,在创建迭代器之后,eval 循环确实运行了。它在获取reversed() 的结果并将其发送到tuple() 时发生。
猜你喜欢
  • 1970-01-01
  • 2023-03-09
  • 1970-01-01
  • 2023-03-30
  • 2014-06-10
  • 2013-12-31
  • 2015-04-23
  • 1970-01-01
  • 2010-12-10
相关资源
最近更新 更多