СТЕНД ДЛЯ ИЗУЧЕНИЯ РАБОТЫ QTHREAD

Один из самых интригующих вариантов реализации многопоточной обработки с помощью QThread описывается в разделе Multithreading Technologies in Qt документации Qt5 следующим образом:
  1. время жизни потока — постоянно, от старта приложения до его завершения,
  2. функционирование — с помощью объекта, живущего в дополнительном потоке, который может выполнять различные задачи по запросу и/или может получать новые данные для работы,
  3. реализация — создать подкласс QObject для создания исполнителя работ, создать экземпляр исполнителя на базе этого подкласса и экземпляр дополнительного потока QThread, переместить экземпляр исполнителя во вновь созданный поток, отправлять команды или данные объекту исполнителя через соединения сигнал-слот с постановкой в очередь (queued connection — см. Qt Namespace).
Самое замечательное здесь то, что и команды, и данные отправляются непосредственно исполнителю.

Вот для того, чтобы разобраться как всё это работает, что можно делать, что нельзя, какие есть ограничения и неожиданности, как со всем этим управляться и где это может найти применение, и была написана программа Зонд дополнительного потока (The Thread Probe).

ТРЕБОВАНИЯ, РЕШЕНИЕ, ИСПОЛНЕНИЕ

Требуется:
  1. чтобы дополнительный поток жил от старта приложения до его завершения,
  2. чтобы объекты, перемещённые в поток, могли неоднократно выполнять различные задания по запросу,
  3. чтобы графический интерфейс позволял пользователю выбирать задания для исполнения,
  4. чтобы всё происходящее в процессе работы приложения немедленно отображалось в графическом интерфейсе с указанием «кто, кому, когда, чего, сколько и на каком основании»,
  5. чтобы по завершению приложения весь накопленный протокол работы сохранялся в файле.
Решено:
  1. дополнительный поток поднять прямо в __main__. С таким временем жизни там ему самое место,
  2. внедрить в поток дополнительный слот, оповещающий пользователя о состоянии потока по запросу,
  3. в поток переместить два объекта-исполнителя. А что мешает? В одном будут задачи, которые рано или поздно сами завершатся, а в другом — те, которые без вмешательства пользователя будут длиться вечно. Заодно можно будет убедиться, что соединения сигнал-слот с постановкой в очередь (queued connection) обслуживают поток в целом, а не отдельные объекты, живущие в нём,
  4. графический интерфейс — главное окно QMainWindow, в котором будет меню со списком задач и центральный виджет QTextEdit с протоколом текущего сеанса работы,
  5. всё управление реализовать в ручном режиме из меню главного окна приложения:
    • получение сведений о дополнительном потоке,
    • запуск, завершение и терминирование дополнительного потока,
    • запрос прерывания дополнительного потока,
    • запуск тестов работы объектов-исполнителей,
    • завершение работы приложения,
  6. по завершению приложения записать протокол в файл в домашнем каталоге.
Сделано:
  1. объект дополнительного потока testThread:
    • testThread.probeTest — сведения о дополнительном потоке,
  2. объект-исполнитель конечных работ testFinite:
    • testFinite.shortTest — короткий тест (5 секунд),
    • testFinite.longTest — длинный тест (30 секунд),
  3. объект-исполнитель бесконечных работ testPerpetual:
    • testPerpetual.tiresomeTest — бесконечный тест,
    • testPerpetual.interruptedTest — бесконечный тест, прерываемый по запросу,
  4. объект главного окна mainWin:
    • mainWin.to_run_probe — запросить сведения о дополнительном потоке,
    • mainWin.to_start_thread — запустить дополнительный поток,
    • mainWin.to_request_interruption — запросить прерывание в дополнительном потоке,
    • mainWin.to_quit_thread — завершить дополнительный поток,
    • mainWin.to_terminate_thread — терминировать дополнительный поток,
    • mainWin.to_run_short — запустить короткий тест (5 секунд),
    • mainWin.to_run_long — запустить длинный тест (30 секунд),
    • mainWin.to_run_tiresome — запустить бесконечный тест,
    • mainWin.to_run_interrupted — бесконечный тест, прерываемый по запросу,
    • mainWin.to_close — завершить работу приложения,
    • mainWin.on_check_started — принят сигнал о запуске дополнительного потока,
    • mainWin.on_check_finished — принят сигнал о завершении дополнительного потока,
    • mainWin.on_check_probe — принят сигнал, содержащий данные, от объекта дополнительного потока,
    • mainWin.on_check_test — принят сигнал, содержащий данные, от объекта-исполнителя работ в дополнительном потоке,
  5. все вышеперечисленные объекты создаются в __main__ в процессе инициализации приложения. Там же привязываются необходимые сигналы к соответствующим слотам,
  6. собранный в сеансе работы протокол дописывается в файл threadprobe.log в домашнем каталоге.

СТРУКТУРА СОБИРАЕМОГО ПРОТОКОЛА

Записи протокола содержат некое описание произошедшего, включая: Объект дополнительного потока и объекты, живущие в дополнительном потоке, формируя нагрузку, отправляемую сигналами, помечают начало каждой строки символами --->, что позволяет отличить их в протоколе от остальных записей. Этой же цели служит и цветовая дифференциация в окне просмотра протокола.

Например,

<0x7f1b9c26c948> mainWin.on_check_started() reported 25/01/2021 10:27:58   
testThread (0x7f1bae0ceb88) started                                        
sender is testThread (0x7f1bae0ceb88)                                      
<0x7f1b9c26c948> mainWin.to_run_short() reported 25/01/2021 10:28:07       
shortSignal emitted                                                        
<0x7f1b9c26c948> mainWin.on_check_test() reported 25/01/2021 10:28:07      
---> <0x7f1bae0ceb88> testFinite.shortTest() reported 25/01/2021 10:28:07  
---> testFinite.shortTest() started                                        
sender is testFinite (0x7f1bae0ceca8)                                      
<0x7f1b9c26c948> mainWin.to_run_probe() reported 25/01/2021 10:28:09       
probeSignal emitted                                                        
<0x7f1b9c26c948> mainWin.on_check_probe() reported 25/01/2021 10:28:09     
---> <0x7f1b9c26c948> testThread.probeTest() reported 25/01/2021 10:28:09  
---> testThread (0x7f1bae0ceb88) is managed from *0x7f1b9c26c948*          
---> isRunning()->True isFinished()->False isInterruptionRequested()->False
sender is testThread (0x7f1bae0ceb88)                                      

Разбирая этот протокол построчно, можно выяснить:
<0x7f1b9c26c948> mainWin.on_check_started() reported 25/01/2021 10:27:58
что объект главного окна mainWin живёт в GUI-потоке с идентификатором 0x7f1b9c26c948,
testThread (0x7f1bae0ceb88) started
что объект дополнительного потока testThread имеет идентификатор 0x7f1bae0ceb88,
---> <0x7f1bae0ceb88> testFinite.shortTest() reported 25/01/2021 10:28:07
что объект-исполнитель работ testFinite живёт в потоке с идентификатором 0x7f1bae0ceb88, т.е. в дополнительном потоке,
---> testThread (0x7f1bae0ceb88) is managed from *0x7f1b9c26c948*
что объект дополнительного потока testThread управляется из потока с идентификатором 0x7f1b9c26c948, т.е. из GUI-потока.

ПРИМЕРНЫЙ СЦЕНАРИЙ РАБОТЫ С ПРИЛОЖЕНИЕМ

Ниже представлена некая последовательность действий, выполняя которую можно проверить и уточнить то, что говорится в документации:
  1. объекты-исполнители выполняются в дополнительном потоке:
    • запустить приложение,
    • запустить дополнительный поток.
      Обратить внимание на идентификатор объекта дополнительного потока testThread. Именно этот идентификатор должны указывать объекты-исполнители в качестве идентификатора потока, в котором они живут,
    • запустить короткий тест.
      Убедиться, что идентификатор потока, в котором живёт объект-исполнитель testFinite совпадает с идентификатором объекта testThread,
  2. выполняющийся тест не мешает запросу состояния потока:
    • запустить прерываемый бесконечный тест,
    • запросить сведения о дополнительном потоке.
      Убедиться, что сведения предоставляются, несмотря на выполняющийся тест, поскольку сам объект дополнительного потока testThread живёт в GUI-потоке,
  3. завершение потока возможно только после завершения теста:
    • завершить дополнительный поток.
      Убедиться, что тест продолжает исполняться,
    • запросить прерывание в дополнительном потоке.
      Убедиться, что поток завершится после завершения теста,
  4. возможен рестарт потока:
    • запросить сведения о дополнительном потоке.
      Убедиться, что поток завершён (isRunning->False и isFinished->True),
    • запустить дополнительный поток,
    • запросить сведения о дополнительном потоке.
      Убедиться, что поток стартовал (isRunning->True и isFinished->False), а запрос прерывания сброшен (isInterruptionRequested->False),
  5. очередь обслуживает поток целиком, а не отдельные объекты:
    • запустить прерываемый бесконечный тест,
    • запустить короткий тест.
      Убедиться, что короткий тест не запустился.
    • запросить прерывание в дополнительном потоке.
      Убедиться, что короткий тест стартует сразу после завершения предыдущего теста.
  6. задания, оставшиеся в очереди после завершения потока, начнут выполняться сразу после рестарта потока:
    • запустить длинный тест,
    • не дожидаясь завершения длинного теста, запустить короткий тест и бесконечный тест, затем завершить дополнительный поток,
    • дождаться завершения длинного теста и завершения потока.
      Убедиться, что ранее запущенные короткий и бесконечный тесты так и не стартовали,
    • запустить дополнительный поток.
      Убедиться, что ранее запущенный короткий тест стартовал,
    • дождаться начала выполнения бесконечного теста и завершить работу приложения.
      Убедиться, что выполняющийся тест не мешает закрытию приложения,
  7. графический интерфейс многопоточен:
    • запустить приложение из командной строки, указав параметр (не важно какой, главное, чтобы он был),
    • просматривая вывод контрольных точек в сессии командной строки, обратить внимание на идентификатор потока, в котором живёт объект главного окна (mainWin, он же [ProbeMainWindow] во время инициализации),
    • поскольку объект главного окна точно живёт в GUI-потоке, то можно убедиться, что GUI перемещается из одного потока в другой в процессе инициализации приложения,
    • запросить сведения о дополнительном потоке,
    • вызвать справку, закрыть её и снова запросить сведения о дополнительном потоке,
    • просматривая полученный протокол работы, обратить внимание на идентификатор потока, в котором живёт объект главного окна mainWin до вызова справки и после него,
    • повторив несколько раз запрос сведений о дополнительном потоке после вызова справки, можно попытаться количественно оценить набор потоков между которыми переключается GUI.
  8. и так далее... Можно, например, попробовать перезапустить поток после терминирования :).

ОБ АВТОРЕ

Автора сего поделия можно найти на Cyberforum.ru под ником iamvic.