Форум программистов, компьютерный форум, киберфорум
iamvic
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  
Путевые заметки в процессе познания Python и PyQt/PySide.
Помни - только тег CODE не портит код добавлением пробела в начало пустой строки.

К вопросу о переключениях GUI с одного потока на другой в приложениях PyQt.

Запись от iamvic размещена 22.12.2021 в 12:31
Показов 1728 Комментарии 4

По определению, GUI в Qt/PyQt живёт в главном потоке, иначе называемом GUI-потоком. Но нельзя сказать, что это будет один и тот же поток в процессе выполнения приложения от старта до завершения. Даже в таком элементарном приложении GUI переключается между потоками по любому поводу:

gui_th_probe.py:
Кликните здесь для просмотра всего текста
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtWidgets import QApplication, QAction
from PyQt5.QtWidgets import QMainWindow, QDialog
 
def print_probe(s):
        print(s, hex(id(QApplication.instance().thread())))
 
 
class Dia_log(QDialog):
    def __init__(self, parent=None):
        super(Dia_log, self).__init__(parent)
        print_probe('      Dia_log.__init__()[1] main_thread_id =')
 
    def closeEvent(self, e):
        print_probe('    Dia_log.closeEvent()[1] main_thread_id =')
        super(Dia_log, self).closeEvent(e)
 
 
class Main_window(QMainWindow):
    def __init__(self, parent=None):
        super(Main_window, self).__init__(parent)
        print_probe('  Main_window.__init__()[1] main_thread_id =')
        self.initUI()
 
    def initUI(self):
        self.statusBar()
        print_probe('    Main_window.initUI()[1] main_thread_id =')
 
        self.dialog_action = QAction(
            'Диалог', self, shortcut='Ctrl+L',
            statusTip='Открыть диалог',
            triggered=self.to_dialog, enabled=True)
        print_probe('    Main_window.initUI()[2] main_thread_id =')
 
        self.exit_action = QAction(
            'Выход', self, shortcut='Ctrl+Q',
            statusTip='Завершить работу приложения',
            triggered=self.to_close, enabled=True)
        print_probe('    Main_window.initUI()[3] main_thread_id =')
 
        self.task_menu = self.menuBar().addMenu('Файл')
        print_probe('    Main_window.initUI()[4] main_thread_id =')
 
        self.task_menu.addAction(self.dialog_action)
        print_probe('    Main_window.initUI()[5] main_thread_id =')
 
        self.task_menu.addSeparator()
        print_probe('    Main_window.initUI()[6] main_thread_id =')
 
        self.task_menu.addAction(self.exit_action)
        print_probe('    Main_window.initUI()[7] main_thread_id =')
 
    def to_dialog(self):
        print_probe(' Main_window.to_dialog()[1] main_thread_id =')
 
        dialog = Dia_log(self)
        print_probe(' Main_window.to_dialog()[2] main_thread_id =')
 
        dialog.exec_()
        print_probe(' Main_window.to_dialog()[3] main_thread_id =')
 
    def to_close(self):
        print_probe('  Main_window.to_close()[1] main_thread_id =')
 
        self.close()
 
    def closeEvent(self, e):
        print_probe('Main_window.closeEvent()[1] main_thread_id =')
 
        super(Main_window, self).closeEvent(e)
 
 
if __name__ == '__main__':
    app = QApplication(sys.argv)
    print_probe('            __main__ app[1] main_thread_id =')
 
    m_win = Main_window()
    print_probe('          __main__ m_win[1] main_thread_id =')
 
    m_win.show()
    print_probe('          __main__ m_win[2] main_thread_id =')
 
    sys.exit(app.exec_())

Запускаем, вооружаемся мышью, выбираем пункт меню Файл -> Диалог, открываем его нажатием ЛКМ, закрываем диалог нажатием на значок в полосе заголовка, выбираем пункт меню Файл -> Выход и закрываем приложение.

В результате получаем следующий протокол работы:
Кликните здесь для просмотра всего текста
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
user@linux:~> python3 gui_th_probe.py
            __main__ app[1] main_thread_id = 0x7fd4854a6e58
  Main_window.__init__()[1] main_thread_id = 0x7fd4854a6ee8
    Main_window.initUI()[1] main_thread_id = 0x7fd4854a6f78
    Main_window.initUI()[2] main_thread_id = 0x7fd47815c048
    Main_window.initUI()[3] main_thread_id = 0x7fd47815c0d8
    Main_window.initUI()[4] main_thread_id = 0x7fd47815c1f8
    Main_window.initUI()[5] main_thread_id = 0x7fd47815c1f8
    Main_window.initUI()[6] main_thread_id = 0x7fd47815c288
    Main_window.initUI()[7] main_thread_id = 0x7fd47815c288
          __main__ m_win[1] main_thread_id = 0x7fd47815c288
          __main__ m_win[2] main_thread_id = 0x7fd47815c288
 Main_window.to_dialog()[1] main_thread_id = 0x7fd47815c288
      Dia_log.__init__()[1] main_thread_id = 0x7fd47815c318
 Main_window.to_dialog()[2] main_thread_id = 0x7fd47815c318
    Dia_log.closeEvent()[1] main_thread_id = 0x7fd47815c3a8
 Main_window.to_dialog()[3] main_thread_id = 0x7fd47815c318
  Main_window.to_close()[1] main_thread_id = 0x7fd47815c288
Main_window.closeEvent()[1] main_thread_id = 0x7fd47815c318
user@linux:~>

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

И всё бы ничего, но вот в моей поделке, выложенной в записи блога К вопросу о применении QSqlDatabase в QThread. для создания уникального имени соединения с базой данных используется идентификатор дополнительного потока. Правомерно ли такое решение? Возможно ли возникновение дубликатов имён соединений?

Конечно, нет! Дубликатов быть не может. Ведь идентификатор однажды созданного объекта дополнительного потока в процессе работы не меняется. Меняется только управляющий им поток. А вот получить при создании идентификатор когда-то существовавшего потока он может. Или, скорее всего, какого-нибудь ранее использовавшегося потока, но теперь ставшего свободным и возвращённого в пул потоков.

С целью подтверждения этих слов приложение было переделано, добавлены необходимые проверки (параноидальные тоже ), новые пункты меню, исправлены ляпы, подчищены затесавшиеся артефакты. Заодно были облагорожены и комментарии для лучшего понимания "что и почему".

Код модуля дополнительного потока теперь выглядит так:

thread_kit.py:
Кликните здесь для просмотра всего текста
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""PostgreSQL in Thread v0.07
From iamvic on www.cyberforum.ru
Restrictions: Python 3.x, PyQt and no more.
"""
 
import copy
 
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import QMutex, QMutexLocker
from PyQt5.QtCore import QObject, QThread
from PyQt5.QtWidgets import QApplication
from PyQt5.QtSql import QSqlDatabase, QSqlQuery
 
class Thread_postgresql(QThread):
    connect_parm = [
        'localhost',    # хост
        '5432',         # порт
        'postgres',     # база данных
        'postgres',     # пользователь
        'postgres'      # пароль
        ]
    thread_mutex = QMutex()
    connection_created = pyqtSignal()
    duplicated_thread = pyqtSignal()
    invalid_using = pyqtSignal()
 
    def set_connection_parm(self, parm):
        '''передать потоку параметры соединения извне
        (для наружного употребления)
        '''
        with QMutexLocker(self.thread_mutex):
            rc = (type(parm) == list and
                  len(parm) == 5 and
                  all(type(x) == str for x in parm) and
                  parm[1].isdigit() and
                  int(parm[1]) in range(1,65535))
            if rc:
                self.connect_parm = copy.deepcopy(parm)
        return rc
 
    def get_connection_parm(self):
        '''получить переданные снаружи параметры соединения
        (для внутреннего употребления)
        '''
        with QMutexLocker(self.thread_mutex):
            rc = copy.deepcopy(self.connect_parm)
        return rc
 
    def run(self):
        '''именно этот код будет выполняться в дополнительном потоке
        после вызова QThread.start()
        '''
        def inner_run(connect_name, current_parm):
            '''поскольку удалить соединение можно только вне области
            видимости соединения, то его создание и вся работа с БД
            выполняется только в теле этой функции, а удаление соединения
            производится снаружи после выхода из неё
            '''
            db = QSqlDatabase.addDatabase('QPSQL', connect_name)
            db.setHostName(current_parm[0])
            db.setPort(int(current_parm[1]))
            db.setDatabaseName(current_parm[2])
            db.setUserName(current_parm[3])
            db.setPassword(current_parm[4])
 
            if not db.open():
                # сигнал об ошибке с информацией из db.lastError().text()
                pass
            else:
                self.connection_created.emit()
                query = QSqlQuery(db)
                if query.exec(
                    "SET application_name TO '{!s}';".format(connect_name)):
                    # хоть какая-то идентификация для админа БД.
                    # QSqlQuery.prepare() с SET не работает
                    # (видимо, о нём не знает).
                    pass
                else:
                    # сигнал об ошибке с информацией из query.lastError().text()
                    pass
 
                for i in range(10):
                    QThread.sleep(1)
                    if self.isInterruptionRequested():
                        # если запрошено прерывание,
                        # то вываливаемся из цикла и завершаем работу
                        break
                # этот цикл моделирует работу с БД, она может сопровождаться
                # периодической отправкой сигналов (если надо),
                # а мы просто спим 10 сек, периодически проверяя
                # наличие запроса на прерывание.
                # self.sleep() не используем, потому как self живёт
                # в основном потоке (мало ли что приключится).
                # в качестве местной экзотики :) можно было бы использовать
                # proxy.thread().sleep(), поскольку вспомогательный
                # объект живёт как раз в дополнительном потоке.
 
            db.close()
            # закрыли БД
            return
            # и вышли из функции
 
        proxy = QObject()
        # именовать соединение будем по идентификатору доп.потока.
        # чтобы убедиться в том, что для запуска использовался именно
        # метод QThread.start(), создаём вспомогательный объект,
        # живущий в дополнительном потоке (self живёт в основном).
        # вспомогательный объект не имеет родителя, поскольку
        # он - первый и, в данном случае, единственный на весь доп.поток.
        # а родитель, как известно, должен жить в том же потоке,
        # что и его отпрыски.
 
        if id(self) == id(proxy.thread()):
            # вспомогательный объект действительно живёт
            # в дополнительном потоке (т.е. в self)
            # => использовали именно QThread.start()
 
            connect_name = 'thread_{!s}_main_{!s}_({!s})'.format(
                # по идентификаторам дополнительного и основного потоков
                # создали уникальное имя соединения вида
                # thread_0x12345678_main_0x1234abcd_(True)
                hex(id(self)),
                # thread_0x12345678_...
                hex(id(self.thread())),
                # ...main_0x1234abcd_...
                (id(QApplication.instance().thread()) ==
                 id(self.thread()) == id(proxy.thread().thread()) )
                # ...(True)
                # (True) подтверждает, что дополнительным потоком
                # управляет именно основной (вот такая паранойя :)
                )
 
            if any(s.split('_')[1] == hex(id(proxy.thread()))
                   for s in QSqlDatabase.connectionNames()):
                # параноидальная проверка с поиском дубликата
                # идентификатора дополнительного потока
                # в именах существующих соединений
                # (совершенно лишняя, но пусть будет)
                # дубликат найден => сообщаем и выходим
                self.duplicated_thread.emit()
 
            else:
                # дубликат не найден => продолжаем работу
                current_parm = self.get_connection_parm()
                # запросили параметры соединения, переданные снаружи
 
                inner_run(connect_name, current_parm)
                # отработали с базой данных
 
                QSqlDatabase.removeDatabase(connect_name)
                # удалили соединение
 
        else:
            # вспомогательный объект живёт в потоке,
            # отличном от дополнительного (т.е. не в self)
            # => использовали QThread.run() вместо QThread.start()
            self.invalid_using.emit()
            # отправляем сигнал о неправильном использовании
 
        pass
        # завершили выполнение потока
        # если дополнительный поток был запущен методом QThread.start(),
        # то это вызовет отправку сигнала QThread.finished()

Остальное традиционно упаковано в прилагаемый архив.

Картинка, которую видит администратор БД:
Нажмите на изображение для увеличения
Название: pit_pgadmin_2.jpg
Просмотров: 476
Размер:	81.6 Кб
ID:	7325

Дополнительные потоки запускались серией нажатий Ctrl-S.

Поскольку записи располагаются в хронологическом порядке, наблюдается интересный эффект - тот, кто был главным при запуске предыдущего дополнительного потока, сам становится дополнительным потоком при следующем запуске. Красным отмечен тот поток, который был главным во время выполнения регистрации в базе данных.

Всех с наступающими праздниками - Рождество, Новый Год и далее по списку!
Самое время для добрых дел...
Вложения
Тип файла: zip pg_th_probe7.zip (9.7 Кб, 502 просмотров)
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 4
Комментарии
  1. Старый комментарий
    Аватар для Avazart
    О Святой Санта и олени с супер силами!
    Очередная порция гавнокода под елкой ... и кто же её навалил !?
    Запись от Avazart размещена 22.12.2021 в 23:21 Avazart вне форума
  2. Старый комментарий
    Эх, Avazart, что ж Вас так плющит-то? Знал я, что ни одно доброе дело не остаётся безнаказанным, но чтобы так...

    Я же не поленился - пошерстил этот форум. Ну, да - лицо Вы тут известное, можно сказать, что в авторитетах ходите, вон какую "репу" наели. Мало того, Вы ведь с этого форума кормитесь, постоянно торча в разделе фриланса. Но в различных дискуссиях в последние годы Вы почему-то всё время пытаетесь оттоптаться на оппонентах... Зазвездились что ли? Или самоутверждаетесь? А может душите паровозы пока они ещё чайники? Но это даже не смешно, я Вам не конкурент.

    Вот если бы Вы привели развёрнутое обоснование своего драгоценного мнения с подробным изложением "что не так и почему" - это было бы полезно. У Вас же опыта больше... А перебрехиваться с Вами в пустопорожнем сраче нет никакого желания. Так что посидите ещё лишенцем без права голоса. Впрочем, личка доступна - делайте выводы...
    Запись от iamvic размещена 23.12.2021 в 07:59 iamvic вне форума
  3. Старый комментарий
    Аватар для kapbepucm
    А для чего появилась необходимость связывать имя соединения БД с идентификатором потока?
    Запись от kapbepucm размещена 16.05.2022 в 09:35 kapbepucm вне форума
  4. Старый комментарий
    kapbepucm, просто в одном из проектов ничего умнее не придумали . Имя-то соединения должно быть уникальным внутри приложения и работающие потоки тоже вроде бы имеют уникальные неизменные идентификаторы. А когда выяснилось, что в дополнительном потоке соединения эпизодически получают имена, совпадающие с именем соединения в главном потоке, то по итогам разборок "гулящим" оказался GUI...
    Запись от iamvic размещена 16.05.2022 в 16:07 iamvic вне форума
 
Новые блоги и статьи
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр.
Jin X 06.09.2026
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр. Работая с форумом и нейросетями в браузере часто хочется что-то подкорректировать или добавить какого-то функционала. Ниже прикреплён. . .
Программа опроса у.з. расходомера SLS-720F
Argus19 02.09.2026
Программа опроса у. з. расходомера SLS-720F Программа опрашивает один раз в минуту три ультразвуковых расходомера SLS-720F через интерфейс RS-485 по протоколу Modbus RTU. Опрашиваются регистры. . .
Hyper-V: Компьютер должен поддерживать доверенный платформенный модуль 2.0.
Maks 31.08.2026
При установке Windows 11 на виртуальную машину Hyper-V 2-го поколения вылезла такая ошибка: Решение: в параметрах виртуальной машины, в разделе "Безопасность" (Security) активировать флаг. . .
Архитектура биовида Стива в Майнкрафте: Зачем бонобо кубический каннибализм
anaschu 30.08.2026
Кубический Вагинокапитализм в Minecraft: Математический инвариант ОДУ и рок Стивов-бонобо Главная задача разработанной «Модели Всего» — наглядно продемонстрировать наличие системной «судьбы». . .
Оттачиваю умение писать js программы.
russiannick 30.08.2026
Проектом выходного дня стало написание Книги шифров Виженера. Итогом стала версия 200, синий туман. Синий туман назван так, потому что замораживает текст под собой. Нажатие синих кнопок управляют. . .
мат медиц модель 30. презентация проекта
anaschu 27.08.2026
хоп хоп хоп хидахоп, а я кладую))
Как у меня протекала болезнь
zorxor 27.08.2026
Здравствуйте, друзья! Эта запись блога предназначена именно для вас - для моих дорогих друзей, которые знали меня лично. Чтобы ответить на вопрос - а что же со мной произошло на самом деле? Я учился. . .
Нашел вот забавное видео о измерениях. Лучшее что я видел на эту тему
kumehtar 26.08.2026
ILETXiw9bMQ Основная суть и тезисы по измерениям: 0D (Нулевое измерение): точка, не имеющая длины, ширины, высоты или объема. Объект не может перемещаться в 0D. 1D (Первое измерение):. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru