为什么 reversed() 会移除线程安全?
Why reversed() removes thread safety?
CPython 中确保迭代线程安全的一个常见习惯用法是使用 tuple()
。
例如 - tuple(dict.items())
在 CPython 中保证是线程安全的,即使项被不同的线程删除也是如此。
这是因为解释器不会运行 eval 循环,并且不会在 运行 运行这些 C 函数时释放 GIL。我已经测试过了,效果很好。
然而,tuple(reversed(dict.items()))
似乎不是线程安全的,我不明白为什么。它不是 运行ning 任何 Python 函数,它没有明确释放 GIL。如果我从 dict
中删除密钥,而它 运行 在不同的线程上,为什么我仍然会收到错误消息?
在迭代时修改 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
时发生的情况:dict 项的 creates a reverse iterator。导致问题的是迭代器部分,因为一旦创建了迭代器,底层字典的大小就不能改变:
>>> 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
CPython 中确保迭代线程安全的一个常见习惯用法是使用 tuple()
。
例如 - tuple(dict.items())
在 CPython 中保证是线程安全的,即使项被不同的线程删除也是如此。
这是因为解释器不会运行 eval 循环,并且不会在 运行 运行这些 C 函数时释放 GIL。我已经测试过了,效果很好。
然而,tuple(reversed(dict.items()))
似乎不是线程安全的,我不明白为什么。它不是 运行ning 任何 Python 函数,它没有明确释放 GIL。如果我从 dict
中删除密钥,而它 运行 在不同的线程上,为什么我仍然会收到错误消息?
在迭代时修改 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
时发生的情况:dict 项的 creates a reverse iterator。导致问题的是迭代器部分,因为一旦创建了迭代器,底层字典的大小就不能改变:
>>> 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