LINUX.ORG.RU

Как закрыть тред при выходе из Tkinter-приложения?

 , , ,


0

2

Имеется Tkinter-приложение, в котором есть:

  • Основное окно
  • Дополнительное окно, которое запускается в отдельном треде. Оно содержит кнопку Panic.

Задача проста: при завершении работы программы любым способом (например, по сочетанию Alt+F4), нужно корректно завершить программу.

Я сделал минимальный пример. При завершении его работы появляется ошибка:

Запускается тред для окна с кнопкой остановки
Внутри треда с окном-кнопкой Panic
Устанавливается флаг на остановку треда с кнопкой Panic
Завершен тред _threadCodeSmallWindow
Тред с кнопкой Panic полностью завершен

Tcl_AsyncDelete: async handler deleted by the wrong thread
Аварийный останов

То есть, нужно, чтобы этой ошибки не возникало.

Если в примере закомментировать последнюю команду main.destroySmallWindow(), то ошибки не будет. Но и программа не будет завершаться по Alt+F4, потому что не завершен тред с дополнительным окном.

Я пробовал перед вызовом rootWindow.mainloop() добавить команду:
rootWindow.protocol("WM_DELETE_WINDOW", onDeleteWindow)

И функцию при закрытии писал такую:
def onDeleteWindow():
    main.destroySmallWindow() # Завершение треда
    rootWindow.destroy() # Закрытие основного окна

Но и тогда все равно ошибка сохраняется.

Вопрос: как корректно завершить программу?

Код примера:
#!/usr/bin/python3

import tkinter as tk
import time
from threading import Thread, Event

class MainFrame(tk.Frame):

    def __init__(self, parent):

        self.stopThreadFlag = Event()
        self.smallWindow = None
        self.threadSmallWindow = None

        # Инициализация базового класса рамки
        super(MainFrame, self).__init__(parent)

        # Основная рамка
        frame = tk.Frame(self, relief=tk.RAISED, borderwidth=1)
        frame.pack(fill=tk.BOTH, expand=True)

        # Надпись
        label = tk.Label(frame, text="Содержимое окна")
        label.pack(anchor=tk.W)

        # Создается окошко-кнопка с кнопкой Panic в отдельном потоке
        self._createSmallWindow()


    # Создание окошка с кнопкой Panic в отдельном потоке
    def _createSmallWindow(self):
        print("Запускается тред для окна с кнопкой остановки")
        self.stopThreadFlag.clear() # Очищается флаг прекращения работы потока
        self.threadSmallWindow = Thread(target = self._threadCodeSmallWindow)
        self.threadSmallWindow.start()


    # Код, выполняемый внутри треда
    def _threadCodeSmallWindow(self):
        print("Внутри треда с окном-кнопкой Panic")

        # Создается окошко с кнопкой Panic
        self.smallWindow= tk.Tk() # = tk.Toplevel(Parameter().rootWindow)
        self.smallWindow.resizable(0, 0) # Запрещает изменение размера
        self.smallWindow.overrideredirect(1) # Отключается все оформление окона

        smallWindowFrame = tk.Frame( self.smallWindow )
        smallWindowFrame.pack(fill="both", expand=True)

        button=tk.Button(smallWindowFrame, text="Panic", borderwidth=0, relief=tk.FLAT)
        button.pack(fill=tk.BOTH, anchor="center", expand=True)

        self.smallWindow.update() # Прорисовка окна с кнопкой Panic

        # Вместо mainloop() используется цикл, который завершается при установке флага
        while not self.stopThreadFlag.is_set():
            if self.smallWindow != None:

                # ... Всякие действия ...

                time.sleep(0.5) # Разгрузка работы, чтобы не выжирался весь процессор

        if self.smallWindow != None:
            self.smallWindow.destroy()

        print("Завершен тред _threadCodeSmallWindow")


    # Уничтожение окна и треда с кнопкой Panic
    def destroySmallWindow(self):
        if self.smallWindow != None:
            print("Устанавливается флаг на остановку треда с кнопкой Panic")
            self.stopThreadFlag.set()

            self.threadSmallWindow.join() # Ожидание завершения треда с кнопкой Panic
            print("Тред с кнопкой Panic полностью завершен")


if __name__ == "__main__":

    # Запуск Tk-подсистемы графических виджетов и создание основного окна
    rootWindow = tk.Tk()
    rootWindow.geometry("320x200+0+0")

    # Создание основной рамки с интерфейсом приложения
    main = MainFrame( rootWindow )
    main.pack(fill="both", expand=True)

    # Основной цикл GUI-интерфейса
    rootWindow.mainloop()

    # Вызов завершения треда с окошком Panic
    # Без этого вызова программа не завершится
    main.destroySmallWindow()

★★★★★

Последнее исправление: Xintrea (всего исправлений: 2)

дождаться пока тред с мелким окном сдохнет, только тогда можно грохать основу

а вообще сверху правы, тк к сожалению работает без косяков только в своем лупе, если хотим в мультипоток, то отказываемся целиком от услуг тк по организации карусельки и следим за всем сами

Morin ★★★★★
()

я в своё время из-за этого перешёл на wxpython. Так как отличные примеры многопоточности в мануалах и полно визуальных редакторов форм, ещё и понравилось.

Shadow ★★★★★
()
Последнее исправление: Shadow (всего исправлений: 2)
Ответ на: комментарий от Morin

тк к сожалению работает без косяков только в своем лупе

? у меня отчего-то Tk нормально работает в разных потоках и разных лупах..и на разных платформах

MKuznetsov ★★★★★
()

ну до чего-же питон говённый язык…

с трудом продираясь :

  1. внутри нити smallWindow, после «destroy .» неплохо вызывать «update» и «update idletasks» ; первое реально поудаляет всё с экрана, по второму отсчёлкают idle

  2. насколько корректно вызывать self.stopThreadFlag.is_set() из другого потока, который прямо сейчас убьётся - вопрос знатокам питона.. я бы mutex и cond wait использовал

  3. у tcl (и tk соотв.) своё представление thread, лучше добавить «thread::exit 0» в конце нитки, чтобы tcl/tk нормально поосвобождал всё своё.

MKuznetsov ★★★★★
()

Я не понял, зачем тут поток, но может поможет:

  1. Можно привязать функцию на закрытие главного окна rootWindow.protocol("WM_DELETE_WINDOW", main.on_close).
  2. У всех объектов есть деструктор __del__.
  3. Диалоги делают с помощью tk.Toplevel(parent).

Можно сделать в главном окне двойную защиту:

    def _on_close(self):
        self.deleting = True
        # stop all
        self.deleted = True

    def on_close(self):
        if not self.deleted:
            self._on_close()
        self.destroy()
        self.parent.destroy()

    def __del__(self):
        if not self.deleted:
            self._on_close()
Kogrom
()
Последнее исправление: Kogrom (всего исправлений: 1)
Ответ на: комментарий от Kogrom

Я не понял, зачем тут поток

чтобы если питоновский поток словит infinite loop, окошко с Panic как-то реагировало на потуги юзера..подсвечивало кнопки и вовремя перерисовывалось :-)

насколько понял замысел: что-то стряслось, запустили тред и больше об его GUI не беспокоимся. Вся проблема ТС - максимально корректно завершить эту нитку с Panic при закрытии приклада.

MKuznetsov ★★★★★
()
Ответ на: комментарий от MKuznetsov

Я же не просто дал минимальный пример, а чтобы отвечающий смог проверить, работают ли его советы.

внутри нити smallWindow, после «destroy .» неплохо вызывать «update» и «update idletasks» ; первое реально поудаляет всё с экрана, по второму отсчёлкают idle

Ну вот я попробовал update() и update_idletasks() в различных комбинациях:

self.smallWindow.destroy()
# self.smallWindow.update()
self.smallWindow.update_idletasks()

В результате эти вызовы сработать не могут:
  File "/usr/lib/python3.9/tkinter/__init__.py", line 1320, in update_idletasks
    self.tk.call('update', 'idletasks')
_tkinter.TclError: can't invoke "update" command: application has been destroyed


у tcl (и tk соотв.) своё представление thread, лучше добавить «thread::exit 0» в конце нитки, чтобы tcl/tk нормально поосвобождал всё своё.

Так то у объекта Thread я не вижу метода exit: https://docs.python.org/3/library/threading.html#thread-objects

И кроме того, основной поток через join() дожидается, когда поток threadSmallWindow завершится.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Aswed

Если тебе нужно просто закрыть приложение, то почему ты не используешь sys.exit? Это прибьет все треды

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

Попробуй написать sys.exit и завершить без ошибки. Возможно, это можно сделать, но знаешь только ты.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Я же не просто дал минимальный пример, а чтобы отвечающий смог проверить, работают ли его советы.

аналог на tcl того что хотите делать :

package require Thread

set child [ thread::create -joinable {
  package require Tk
  set do_exit 0
  wm protocol . WM_DELETE_WINDOW { set do_exit 1 }
  button .pressme -text "pressme" -command { set do_exit 1 }
  pack .pressme
  while {!$do_exit} {
     update
     update idletasks
     after 50
  }
  catch { destroy . }
  update
  thread::exit 0
} ]

thread::join $child
puts "The end"
exit

переведите на питон или поищите ошибки в своей реализации.

MKuznetsov ★★★★★
()
Ответ на: комментарий от MKuznetsov

аналог на tcl того что хотите делать

С чего вы взяли, что реализация тредов в Python и Tcl адекватно соответствует друг другу? С чего вы взяли, что объекты Tk, будучи размещенные через питоновские треды, будут вести себя так же, как и в Tcl реализации? Учитывая, что в питоне не вызываются деструкторы по тем же принципам, как в других языках?

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Ну у тебя есть отдельный тред с окном, по закрытию которого надо закрыть приложение. Внутри этого треда ты можешь поймать on_close событие. У тебя же там отдельный mainloop, да? Ну и обработать его с sys.exit(0).

Кстати забыл спросить, почему вообще для этого нужен отдельный тред? К ткинтеру же можно быстро прикрутить asyncio и крутить mainloop как async функцию. Тогда ты можешь напихать несколько таких в один тред. И в отличии от треда отслеживать состояние корутин просто и отменять их можно при необходимости.

Aswed ★★★★★
()
Ответ на: комментарий от Xintrea

С чего вы взяли, что реализация тредов в Python и Tcl адекватно соответствует друг другу? С чего вы взяли, что объекты Tk, будучи размещенные через питоновские треды, будут вести себя так же, как и в Tcl реализации? Учитывая, что в питоне не вызываются деструкторы по тем же принципам, как в других языках?

с того что tkinterp всего-лишь взаимодействует с tcl/tk. Создаёт внутри себя Tcl_Interp https://www.tcl-lang.org/man/tcl8.6/TclLib/Interp.htm и дёргает его API.

на самом деле скорее всего вы заблудились в питоне и потоках. Многопоток возможно пока не ваше. Впрочем как и питон, ваш код читается с большим трудом.

MKuznetsov ★★★★★
()

про tk ничего не знаю. вообще судя по тому что пишут, оно может нормально работать только если работа с ним из одного треда, то есть вообще тебе надо как-то переделать чтобы ui-тред только один был (и вполне возможно, что оно захочет, чтобы этот один тред был именно мейн-тредом). только вот можно ли у него на два окна один эвент-луп иметь я не знаю. то есть события-то уже с эвент-лупа обработать другим тредом не проблема понятное дело, но вот можно ли их принять в одном эвент-лупе я понятния не имею.

и еще такой фикс тоже вроде работает:

@@ -63,6 +63,10 @@ class MainFrame(tk.Frame):
         if self.smallWindow != None:
             self.smallWindow.destroy()
 
+        self.smallWindow = None
+        import gc
+        gc.collect()
+
         print("Завершен тред _threadCodeSmallWindow")

https://discuss.python.org/t/tcl-asyncdelete-async-handler-deleted-by-the-wrong-thread/11872

asdpm
()
Ответ на: комментарий от MKuznetsov

на самом деле скорее всего вы заблудились в питоне и потоках. Многопоток возможно пока не ваше. Впрочем как и питон, ваш код читается с большим трудом.

Возможно, что вы просто обосрались, когда в технической теме решили перейти на личности и начали рассуждать о знаниях незнакомого вам человека. Вам стоит пойти подмыться и отстирать штаны, потому что находиться рядом с вами даже в интернет-форуме нормальным людям неприятно, воняет через монитор.

Теперь по вашим знаниям. Вы никакого решения предложить не смогли, несмотря на то, что для вас был дан минимальный рабочий пример. Вы рассуждали про Tk, зачем-то в теме про питон начали писать на голом Tcl, стали фантазировать про сложности многопотока. И все это вместо того, чтобы написать пару строчек кода, который родить вы не смогли. Это удручает, и показывает вас как пустобрёха.

Так же советую почитать общение разработчиков по задаче 39093 которое нашел asdpm, и понять, что вы гнали нелепые советы, не понимая сути проблемы.

Писать в эту ветку форума вам больше не стоит.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от asdpm

и еще такой фикс тоже вроде работает:
+ self.smallWindow = None
+ import gc
+ gc.collect()

Да, работает, спасибо тебе, дорогой товарищ.

Понятно, что это workaround проблемы. Но тут вообще удивительно что из контролируемой среды (пользовательский Python-код) можно пофиксить внутреннюю проблему инфраструктурного кода, я б до такого не додумался.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от MKuznetsov

насколько понял замысел: что-то стряслось, запустили тред и больше об его GUI не беспокоимся.

Это говнокод уровня deplhi 1 курс, button1_click(как впрочем и код ТС).

GUI тред в приложении должен быть один. В macOS кстати это чуть ли не вшито в логику системных фреймворков, то что UI поток один и более того, это main thread. На винде тоже некоторые компоненты COM требуют именно Main STA для UI, особенно написанные на дельфи или подобной поебени(сам сталкивался с могучей библиотечкой написанном европейцами вот которая нормально не работала кроме как из main thread).

Более того, Tkinter - в принципе однопоточный.

GUI тред собственно должен работать только с UI. Все остальное должно работать в бэкграунде, в воркере(или нескольких) и коммуникация с GUI тредом должна быть асинхронная. Это именно для того, чтобы UI не вис в принципе. И чтобы его всегда можно было закрыть.

Тут встает конечно два вопроса - первый, это возможно ли в Tk создать два окошка верхнего уровня(Toplevel или как он его) для одного event loop. Скорее всего нет, тогда придется либо переделывать UI чтобы кнопку тупо запихнуть в layout, либо использовать другой GUI фреймворк. Второй - я собственно давно не лез в питон, но вот из-за GIL, не случится ли там зависание всего процесса если повис один тред?

lovesan ★★★
()
Ответ на: комментарий от MKuznetsov

идиоты… вы объект созданный в одном треде, удаляёте в другом, после завершения оного треда… питонисты блджатъ

Не мы, а авторы реализации Tk в Python не учитывающие особенности гарбажколлектора. Питонисты вообще не должны задумываться о гарбажколлекторе и писать воркароунды, на то он и управляемый скриптовый язык. Про какое удаление ты бормочешь, если в питоне даже деструкторы в явном виде не вызываются, непонятно.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Питонисты вообще не должны задумываться о гарбажколлекторе и писать воркароунды

Это если они умственно отсталые(как впрочем большинство любителей питона). Потому что то что у тебя GC не значит что у тебя в программе нет неуправляемых ресурсов - таких как файловые дескрипторы или ресурсы сишных библиотек(такие как GUI окошки) это раз, а два, даже с GC для критических ресурсов ты должен писать финализаторы и продумывать логику их работы это раз, и два, даже с GC могут быть утечки памяти, особенно у любителей глобальных переменных(таких как питонисты).

lovesan ★★★
()
Ответ на: комментарий от asdpm

Кстати, я поэкспериментировал, и в моем случае гарбажколлектор не обязательно в явном виде вызывать. Чтобы гарбажколлектор правильно сработал, можно просто убрать последнюю ссылку на маленькое окно self.smallWindow = None в треде, и тогда гарбажколлектор грохнет окно раньше, т. е. пока тред существует.

Вот только с такими закидонами я даже не знаю, можно ли однозначно сказать что гарбажколлектор обязательно сработает вовремя. По логике вещей должен сразу отследить освобождение ссылки. Но хрен его знает как на деле реализовано, такие подробности выше моих компетенций.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

ну да. просто мы же не знаем гарантировано ли он вызовется при шатдауне треда. так то вызов этих финализаторов вообще не гарантирован питоном как языком

asdpm
()
Ответ на: комментарий от Xintrea

Не мы, а авторы реализации Tk в Python не учитывающие особенности гарбажколлектора.

это не только коллектор..вы своими руками в отдельном треде запустили tkinter. Там Tkinter создал интерпретатор tcl и подгрузил туда tk. Чтобы работать с GUI, и организовать event loop, ему бедолаге приходится использовать thread-specific-variables или __thread и как-то взаимодействовать с X и так далее. При удалении интерпретатора, в обратную сторону (взять tsv, удалить/закрыть/)

У вас получилось что сначала вы бахнули его тред где он жил, и только потом взялись удалять интерпретатор tcl

MKuznetsov ★★★★★
()
Ответ на: комментарий от lovesan

Более того, Tkinter - в принципе однопоточный.

можно сказать да. Но по крайней мере на уровне tk - thread-safe. То есть можно в разных нитях запускать отдельные инстанцы и ничего страшного не будет, будет больше окошек :-). Просто не надо из других нитей дёргать методы tkinter.

В tcl/tk это ограничение просто обходится: thread::send $thread { wm deiconify . } . В питоне наверное есть схожий аналог

Тут встает конечно два вопроса - первый, это возможно ли в Tk создать два окошка верхнего уровня(Toplevel или как он его) для одного event loop. Скорее всего нет, тогда придется либо переделывать UI чтобы кнопку тупо запихнуть в layout, либо использовать другой GUI фреймворк

в tk конечно-же можно создавать столько toplevel сколько нужно. Не ограничивается.

И самих «tk» можно насоздавать хренову гору, по пачке на тред, ворох тредов на процесс. Только управлять этой горой будет геморойно, поэтому типично - один GUI инстанс (то есть это не техническое ограничение)

MKuznetsov ★★★★★
()