В этом посте я покажу решение проблемы и что привело меня к ее обнаружению. Он включает в себя просмотр кода CPython _tkinter.c
, поэтому, если это не то, что вам нужно, вы можете просто перейти к разделу TL; DR ниже. А теперь давайте погрузимся в кроличью нору.
Lead-Up
Проблема возникает только при ручном перемещении ползунка. MainThread
и Video
-поток тогда находятся в взаимоблокировке друг с другом через LOCK
, который я назову пользовательской блокировкой. Теперь метод run
никогда не освобождает пользовательскую блокировку после того, как is получил ее, что означает, что она зависает, потому что ожидает другой блокировки или выполнения какой-либо операции, которая не может. Теперь, глядя на вывод журнала вашего подробного примера, становится ясно, что программа не зависает постоянно: требуется несколько попыток.
Добавив больше отпечатков в метод run
, вы можете обнаружить, чтопроблема не всегда вызвана get
или set
. Когда проблема вызвана, get
, возможно, уже закончил, или это может не иметь. Это означает, что проблема вызвана не get
или set
, а скорее каким-то более общим механизмом.
Variable.set и Variable.get
В этом разделе я рассмотрел только код Python 2.7, хотя эта проблема также присутствует в Python 3.6. Из класса Variable
в файле Tkinter.py
CPython 2.7:
def set(self, value):
"""Set the variable to VALUE."""
return self._tk.globalsetvar(self._name, value)
def get(self):
"""Return value of variable."""
return self._tk.globalgetvar(self._name)
Атрибут self._tk
является Tk-объектом, определенным в C-коде Tkinterи для кода globalgetvar
мы должны вернуться к _tkinter.c
:
static PyObject *
Tkapp_GlobalGetVar(PyObject *self, PyObject *args)
{
return var_invoke(GetVar, self, args, TCL_LEAVE_ERR_MSG | TCL_GLOBAL_ONLY);
}
Перейти к var_invoke
:
static PyObject*
var_invoke(EventFunc func, PyObject *selfptr, PyObject *args, int flags)
{
#ifdef WITH_THREAD
// Between these brackets, Tkinter marshalls the call to the mainloop
#endif
return func(selfptr, args, flags);
}
Просто чтобы убедиться: я скомпилировал Python с поддержкой потоков, и проблема не исчезла. Вызов направляется в основной поток, который я проверил с помощью простого printf
в этом месте. Теперь это сделано правильно? Функция var_invoke
будет ожидать возобновления MainThread и выполнения запрошенного вызова. Что делает MainThread на этом этапе? Ну, он выполняет свою очередь событий в той последовательности, в которой он их получил. В какой последовательности он их получил? Это зависит от сроков. Вот что вызывает проблему: в некоторых случаях Tkinter выполнит вызов обратного вызова прямо перед get
или set
, но пока удерживается блокировка.
Независимо от того, является ли mtTkinter
импортируется (пока Python скомпилирован с поддержкой WITH_THREAD
), вызовы get
и set
перенаправляются в mainloop, но этот mainloop может в этот момент просто пытаться вызвать callback, который также нуждается вблокировка ... Это то, что вызывает тупик и вашу проблему. Таким образом, в основном mtTkinter
и обычный Tkinter предлагают то же самое поведение, хотя для mtTkinter
такое поведение вызывается в коде Python, а для простого Tkinter это срабатывает в C-коде.
TL; DR;Короче говоря
Проблема вызвана только пользовательской блокировкой. Ни GIL, ни блокировка Tcl-интерпретатора не задействованы. Проблема вызвана тем, что методы get
и set
направляют свой фактический вызов на MainThread
и затем ожидают завершения вызова этим MainThread
, в то время как MainThread
пытается сделать события в порядкеи сначала выполните обратный вызов.
Это предполагаемое поведение? Может быть, я не уверен. Я уверен, что вижу, что со всеми макросами ENTER_TCL
и LEAVE_TCL
в файле _tkinter.c
возможно лучшее решение, чем текущее. На данный момент, однако, нет никакого реального обходного пути для этой проблемы (ошибка? Функция?), Которую я вижу, кроме использования Tk.after(0, Variable.set)
, так что Video
-поток не удерживает блокировку, пока MainThread
может понадобитьсяМое предложение будет удалять вызовы DoubleVar.get
и set
из кода, в котором удерживается блокировка. В конце концов, если ваша программа делает что-то значимое, ей может не потребоваться удерживать блокировку, пока она устанавливает DoubleVar
. Или, если это не вариант, вам нужно будет найти другие способы синхронизации значения, например, подкласс DoubleVar
. То, что лучше всего соответствует вашим потребностям, во многом зависит от вашего фактического применения.