Мне часто приходится анализировать проблемы с многопоточностью в python коде, и до сих пор я думал, что понимаю, как выяснить, может ли фрагмент кода высвободить GIL и, следовательно, вызвать условия гонки. Однако, проанализировав следующий фрагмент кода, я думаю, что, должно быть, упустил что-то, чтобы выяснить, когда GIL может быть выпущен.
import sys
for a, b in list(sys.modules.items()):
do_something()
Это, очевидно, может привести к редкому RuntimeError: dictionary changed size during iteration
из-за того, что sys.modules
изменено в другой теме. Сейчас мы столкнулись с этим несколько раз из сотен серий, поэтому воспроизвести его не так просто. Несмотря на исправление, которое я считаю поточнобезопасным (sys.modules.copy().items()
), я пытался понять, какая часть утверждения позволила изменить dict, пока мы перебираем представление dict. Во время анализа того, какая часть утверждения может фактически быть проблемой, я понял, что могу упустить некоторое понимание GIL.
Мои шаги анализа следующие:
1.) Упростите проблема в минимальном примере:
import dis
a = {}
def f():
for i, j in list(f.items()):
pass
def g():
for i, j in f.copy().items():
pass
print("Function f:")
dis.dis(f)
print("Function g:")
dis.dis(g)
, который показывает, что функция f может сбрасывать GIL между вызовом элементов и вызовом списка.
Function f:
5 0 SETUP_LOOP 24 (to 26)
2 LOAD_GLOBAL 0 (list)
4 LOAD_GLOBAL 1 (f)
6 LOAD_METHOD 2 (items)
8 CALL_METHOD 0
10 CALL_FUNCTION 1
12 GET_ITER
>> 14 FOR_ITER 8 (to 24)
16 UNPACK_SEQUENCE 2
18 STORE_FAST 0 (i)
20 STORE_FAST 1 (j)
6 22 JUMP_ABSOLUTE 14
>> 24 POP_BLOCK
>> 26 LOAD_CONST 0 (None)
28 RETURN_VALUE
Function g:
9 0 SETUP_LOOP 24 (to 26)
2 LOAD_GLOBAL 0 (f)
4 LOAD_METHOD 1 (copy)
6 CALL_METHOD 0
8 LOAD_METHOD 2 (items)
10 CALL_METHOD 0
12 GET_ITER
>> 14 FOR_ITER 8 (to 24)
16 UNPACK_SEQUENCE 2
18 STORE_FAST 0 (i)
20 STORE_FAST 1 (j)
10 22 JUMP_ABSOLUTE 14
>> 24 POP_BLOCK
>> 26 LOAD_CONST 0 (None)
28 RETURN_VALUE
2.) Удивлен, что изменение диктат после создания объекта представления может вызвать исключение. Я проверил это предположение, но оказалось, что оно работает нормально:
a = {1: 1}
b = a.items()
a[2] = 2
for i, j in list(b):
print(i)
1
2
3.) Следующим шагом является попытка увидеть, что на самом деле происходит в cpython. Фактическое создание списка из представления dict происходит в Objects/listobject.c:list_extend
, который создает итерацию с PyObject_GetIter(iterable)
и после некоторых вызовов использует прямые вызовы C для запуска через итератор путем вызова tp_iternext
для объекта итератора без освобождения GIL.
В list_extend
единственная функция, которая выглядит так, как будто она может выпустить GIL в какой-то момент, - это PyObject_LengthHint
, которая на самом деле вызывает _PyObject_CallNoArg
, но отладка показывает, что код на самом деле принимает кодовый путь _PyObject_HasLen
и, следовательно, не должен выпускать GIL.
Если я не понимаю, что GIL может быть сброшен в коде C, только если мы на самом деле вызываем функцию с помощью одного из методов PyObject_Call*
, это неправильно, я не понимаю, как приведенный выше код может фактически вызвать исключение.
Мои два вопроса после анализа этого пути к коду: какое из моих предположений о GIL неверно, и если есть лучший способ выяснить, какие методы python не являются сбросив GIL.