Почему эта программа tkinter зависает? - PullRequest
1 голос
/ 28 октября 2019

У меня проблема с зависанием моего графического интерфейса, и я не знаю почему. Метод run не снимает блокировку.

Демонстрационная программа

import time
import threading
import Tkinter as tk
import ttk

LOCK = threading.Lock()

class Video(threading.Thread):
    def __init__(self):
        super(Video, self).__init__()
        self.daemon = True
        self.frame = tk.DoubleVar(root, value=0)
        self.frames = 1000

    def run(self):
        while True:                
            with LOCK:   
                position = self.frame.get()

                if position < self.frames:
                    position += 1
                else:
                    position = 0

                self.frame.set(position)                

            time.sleep(0.01)

root = tk.Tk()
video = Video()
root.minsize(500, 50)

def cb_scale(_):
    with LOCK:
        print('HELLO')

scale = ttk.Scale(
    root, from_=video.frame.get(), to=video.frames, variable=video.frame,
    command=cb_scale)

scale.grid(row=0, column=0, sticky=tk.EW)
root.columnconfigure(0, weight=1)

if __name__ == '__main__':
    video.start()
    root.mainloop()

Проблема

Спам-щелчокиндикатор выполнения останавливает программу.

попытки отладки

  1. Я использовал mttkinter, добавив import mttkinterк операторам импорта, и проблема сохраняется. Проблема в том, что блокировка не снимается.

  2. Я вставил операторы печати, чтобы узнать, где именно программа зависает.

Программа с операторами печати:

from __future__ import print_function

import time
import threading
import Tkinter as tk
import ttk

def whichthread(say=''):
    t = threading.current_thread()
    print('{}{}'.format(say, t))

LOCK = threading.Lock()

class Video(threading.Thread):
    def __init__(self):
        super(Video, self).__init__()
        self.daemon = True
        self.frame = tk.DoubleVar(root, value=0)
        self.frames = 1000

    def run(self):
        while True:
            whichthread('run tries to acquire lock in thread: ')
            with LOCK:
                whichthread('run acquired lock in thread: ')

                position = self.frame.get()

                if position < self.frames:
                    position += 1
                else:
                    position = 0

                self.frame.set(position)
            whichthread('run released lock in thread: ')

            time.sleep(0.01)

root = tk.Tk()
video = Video()
root.minsize(500, 50)

def cb_scale(_):
    whichthread('cb_scale tries to acquire lock in thread: ')
    with LOCK:
        whichthread('cb_scale acquired lock in thread: ')
        print('HELLO')
    whichthread('cb_scale released lock in thread: ')

scale = ttk.Scale(
    root, from_=video.frame.get(), to=video.frames, variable=video.frame,
    command=cb_scale)

scale.grid(row=0, column=0, sticky=tk.EW)
root.columnconfigure(0, weight=1)

if __name__ == '__main__':
    video.start()
    root.mainloop()

Это приводит к следующему выводу непосредственно перед зависанием программы:

...
run tries to acquire lock in thread: <Video(Thread-1, started daemon 140308329449216)>
run acquired lock in thread: <Video(Thread-1, started daemon 140308329449216)>
cb_scale tries to acquire lock in thread: <_MainThread(MainThread, started 140308415592256)>

Это показывает, что по какой-то причине метод run не снимает блокировку.

Я попытался закомментировать строки, чтобы сузить проблему.

Удаление любого из двух операторов with LOCK решает проблему. К сожалению, в моей настоящей программе функции run и cb_scale делают что-то значимое, что требует блокировки.

Комментирование обоих вызовов к get и set в run исправляетвыпуск.

... и вот где я застрял! :)

РЕДАКТИРОВАТЬ

Благодаря Майк - SMT Мне удалось отследить проблему дальше.

Использование

class DummyDoubleVar(object):
    def get(self):
        return 500

    def set(self, _):
        pass

и

self.frame = DummyDoubleVar()

в Video.__init__ предотвращает зависание программы.

(Помните, что оригинальная программа надежно зависает даже при mttkinter. Я в замешательстве, что здесь происходит!)

Ответы [ 2 ]

1 голос
/ 08 ноября 2019

В этом посте я покажу решение проблемы и что привело меня к ее обнаружению. Он включает в себя просмотр кода 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. То, что лучше всего соответствует вашим потребностям, во многом зависит от вашего фактического применения.

1 голос
/ 28 октября 2019

Я не знаю на 100%, почему ваша программа блокируется при нажатии на ползунок, однако я подозреваю, что это связано с tk.DoubleVar(), поскольку это является частью основного потока.

Рассмотрите возможность использования after() вместо.

См. пример ниже и дайте мне знать, если у вас есть какие-либо вопросы.

import tkinter as tk
import tkinter.ttk as ttk


class Video(tk.Tk):
    def __init__(self):
        super().__init__()
        self.minsize(500, 50)
        self.daemon = True
        self.frames = 1000
        self.columnconfigure(0, weight=1)
        self.vid_var = tk.DoubleVar(self, value=0)
        scale = ttk.Scale(self, from_=self.vid_var.get(), to=self.frames, variable=self.vid_var, command=self.cb_scale)
        scale.grid(row=0, column=0, sticky='ew')
        self.run()

    def cb_scale(self, var):
            print('HELLO', var)

    def run(self):
        position = self.vid_var.get()
        if position < self.frames:
            position += 1
            self.after(10, self.run)
        else:
            position = 0
        self.vid_var.set(position)


if __name__ == '__main__':
    Video().mainloop()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...