Как интегрировать убиваемые процессы/потоки в графический интерфейс Python?

Добрый день, я действительно новичок в python, и передо мной стоит задача, которую я не могу полностью понять.

Я создал интерфейс с Tkinter, который должен выполнить пару, казалось бы, простых действий. При нажатии кнопки «Пуск» будут запущены два потока/процесса (каждый из которых вызывает несколько подфункций), которые в основном считывают данные из последовательного порта (конечно, один порт на процесс) и записывают их в файл. Действия ввода-вывода зациклены в цикле while с очень высоким значением счетчика, что позволяет им продолжаться практически бесконечно.

Кнопка «Стоп» должна остановить сбор данных и, по сути, должна:

  1. Убить поток чтения/записи
  2. Закрыть файл
  3. Закройте последовательный порт

К сожалению, я до сих пор не понимаю, как выполнить пункт 1, то есть как создать убиваемые потоки, не убивая весь графический интерфейс. Есть ли способ сделать это?

Спасибо вам всем!


person Mutewinter    schedule 15.12.2015    source источник


Ответы (2)


Во-первых, вы должны выбрать, собираетесь ли вы использовать потоки или процессы.

Я не буду слишком углубляться в различия, погуглите ;) В любом случае, вот некоторые вещи, которые следует учитывать: гораздо проще установить связь между потоками, чем между процессами; в Python все потоки будут выполняться на одном и том же ядре ЦП (см. Python GIL), но подпроцессы могут использовать несколько ядер.

Процессы

Если вы используете подпроцессы, есть два способа: subprocess.Popen и multiprocessing.Process. С Popen вы можете запускать что угодно, тогда как Process предоставляет более простой потоковый интерфейс для запуска кода Python, который является частью вашего проекта в подпроцессе.

Оба могут быть убиты методом terminate.

См. документацию для multiprocessing и subprocess

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

Обсуждения

Для потоков вы должны реализовать свой собственный механизм остановки, а не использовать что-то столь же жестокое, как process.terminate().

Обычно поток запускается в цикле, и в этом цикле вы проверяете наличие флага stop. Тогда вы выходите из петли.

У меня обычно что-то вроде этого:

class MyThread(Thread):
    def __init__(self):
        super(Thread, self).__init__()
        self._stop_event = threading.Event()

    def run(self):
        while not self._stop_event.is_set():
            # do something
            self._stop_event.wait(SLEEP_TIME)
        # clean-up before exit

    def stop(self, timeout):
        self._stop_event.set()
        self.join(timeout)

Конечно, вам нужна некоторая обработка исключений и т. д., но это основная идея.

EDIT: ответы на вопросы в комментариях

thread.start_new_thread(your_function) запускает новый поток, это правильно. С другой стороны, модуль threading предоставляет API более высокого уровня, который намного удобнее.

С модулем threading вы можете сделать то же самое с:

t = threading.Thread(target=your_function)
t.start()

или вы можете создать свой собственный класс, который наследуется от Thread, и поместить свою функциональность в метод run, как в примере выше. Затем, когда пользователь нажимает кнопку запуска, вы делаете:

t = MyThread()
t.start()

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

Когда пользователь нажимает «Стоп», вы должны:

t.stop(some_reasonable_time_in_which_the_thread_should_stop)

После этого вы можете удалить t из своего списка, его больше нельзя использовать.

person zvone    schedule 15.12.2015
comment
Спасибо за развернутый ответ! Я думаю, что тогда я упускаю из виду основной смысл потоковой передачи. В моем случае функция с циклом while создается в потоке thread.start_new_thread(function_with_while), но вместо этого я должен зациклить поток, а не саму функцию. Это правильно? И как в этом случае передать потоку стоп-флажок? Поток будет появляться при нажатии кнопки «Пуск», но остановка обрабатывается другим объектом Tkinter (кнопка «Стоп»). Достаточно ли сделать поток глобальным? - person Mutewinter; 15.12.2015
comment
спасибо, но дело в том, что когда пользователь нажимает кнопку «Стоп», поток должен узнать, что флаг остановки был поднят, и остановиться, в противном случае продолжайте получать данные через последовательный порт. Нет разумного времени для остановки, потому что он должен продолжать приобретать, если не указано иное. Это та часть, которую я нахожу действительно сложной, и она не позволяет мне вставлять точки сна на этапе получения, иначе я потеряю пакеты из порта... - person Mutewinter; 15.12.2015
comment
Аргумент timeout для stop является необязательным. Вы можете установить его на None. См. документацию по Thread.join(). Если вы хорошо реализуете свой метод запуска, это немедленно остановится. Причина наличия тайм-аута в том, что вы можете обрабатывать проблемы с потоком. Например. если у вас есть ошибка в run, из-за которой он никогда не останавливается, вызов stop (который вызывает join) заморозит основной поток. Тайм-аут сообщает функции, когда сдаться, если поток не отвечает. Что вы будете делать в этом случае, опять же зависит от вас. - person zvone; 15.12.2015
comment
спасибо, это довольно ясно, на самом деле это именно то, что происходит: я реализовал свой поток как класс потока (то есть: MyThread(threading.Thread)) и определил атрибут соединения (он так называется в Python?): тайм-аут отсутствует, и весь графический интерфейс зависает когда я нажимаю кнопку "Стоп" и останавливаю поток... - person Mutewinter; 16.12.2015
comment
Thread.join() мало что делает. Это просто означает дождаться завершения этого потока. Это не приводит к остановке потока. Вы должны сказать ему остановиться, а затем подождать, пока он остановится. Кстати, это хорошее объяснение, почему он принимает аргумент timeout - смысл в том, что ждите завершения этого потока, но не ждите дольше, чем X секунд - person zvone; 16.12.2015

Сначала вы можете использовать subprocess.Popen() для порождения дочерних процессов, а затем вы можете использовать Popen.terminate() для их завершения.

Обратите внимание, что вы также можете сделать все в одном потоке Python, без подпроцессов, если хотите. Вполне возможно «мультиплексировать» чтение из нескольких портов в одном цикле событий.

person John Zwinck    schedule 15.12.2015