Resumo : neste tutorial, você aprenderá como criar um aplicativo multithreading PyQt que usa QThreadPool
classes QRunnable
.
Introdução às classes QThreadPool e QRunnable
A QThread
classe permite transferir uma tarefa de longa execução para um thread de trabalho para tornar o aplicativo mais responsivo. A classe QThread funciona bem se o aplicativo tiver alguns threads de trabalho.
Um programa multithread é eficiente quando possui um número de QThread
objetos que corresponde ao número de núcleos da CPU.
Além disso, a criação de threads é bastante cara em termos de recursos do computador. Portanto, o programa deve reutilizar os threads criados tanto quanto possível.
Portanto, usar a QThread
classe para gerenciar threads de trabalho apresenta dois desafios principais:
- Determine o número ideal de threads para o aplicativo com base no número de núcleos da CPU.
- Reutilize e recicle os threads tanto quanto possível.
Felizmente, o PyQt tem
aulas que resolvem esses desafios para você. A QThreadPool
QThreadPool
classe é frequentemente usada com a QRunnable
classe.
- A
QRunnable
classe representa uma tarefa que você deseja executar em um thread de trabalho. - O
QThreadPool
executa umQRunnable
objeto e gerencia e recicla threads automaticamente.
Cada aplicação Qt possui um
objeto global que pode ser acessado através do QThreadPool
globalInstance()
método estático da
classe.QThreadPool
Para usar as classes QThreadPool
e QRunnable
, siga estas etapas:
Primeiro, crie uma classe que herde da QRunnable
classe e substitua o run()
método:
class Worker(QRunnable):
@Slot()
def run(self):
# place a long-running task here
pass
Linguagem de código: Python ( python )
Segundo, acesse o pool de threads na janela principal e inicie os threads de trabalho:
class MainWindow(QMainWindow):
# other methods
# ...
def start(self):
""" Create and execute worker threads
"""
pool = QThreadPool.globalInstance()
for _ in range(1, 100):
pool.start(Worker())
Linguagem de código: Python ( python )
Para atualizar o progresso do trabalhador para o thread principal, você usa sinais e slots. No entanto, o QRunnable
não suporta sinal.
Portanto, você precisa definir uma classe separada que herde QObject
e use essa classe na classe Worker. Aqui estão as etapas:
Primeiro, defina a Signals
classe que subclassifica a QObject
classe:
class Signals(QObject):
completed = Signal()
Linguagem de código: Python ( python )
Na Signals
classe, definimos um sinal chamado completed
. Observe que você pode definir quantos sinais forem necessários.
Segundo, emita o completed
sinal quando o trabalho for concluído na Worker
aula:
class Runnable(QRunnable):
def __init__(self):
super().__init__()
self.signals = Signals()
@Slot()
def run(self):
# long running task
# ...
# emit the completed signal
self.signals.completed.emit()
Linguagem de código: Python ( python )
Terceiro, conecte o sinal do thread de trabalho a um slot da janela principal antes de enviar o trabalhador ao pool:
class MainWindow(QMainWindow):
# other methods
# ...
def start(self):
""" Create and execute worker threads
"""
pool = QThreadPool.globalInstance()
for _ in range(1, 100):
worker = Worker()
worker.signals.completed.connect(self.update)
pool.start(worker)
def update(self):
# update the worker
pass
Linguagem de código: Python ( python )
Exemplo de QThreadPool
O exemplo a seguir ilustra como usar a classe QThreadPool
and QRunnable
:
import sys
import time
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QGridLayout, QWidget, QProgressBar, QListWidget
from PyQt6.QtCore import QRunnable, QObject, QThreadPool, pyqtSignal as Signal, pyqtSlot as Slot
class Signals(QObject):
started = Signal(int)
completed = Signal(int)
class Worker(QRunnable):
def __init__(self, n):
super().__init__()
self.n = n
self.signals = Signals()
@Slot()
def run(self):
self.signals.started.emit(self.n)
time.sleep(self.n*1.1)
self.signals.completed.emit(self.n)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle('QThreadPool Demo')
self.job_count = 10
self.comleted_jobs = []
widget = QWidget()
widget.setLayout(QGridLayout())
self.setCentralWidget(widget)
self.btn_start = QPushButton('Start', clicked=self.start_jobs)
self.progress_bar = QProgressBar(minimum=0, maximum=self.job_count)
self.list = QListWidget()
widget.layout().addWidget(self.list, 0, 0, 1, 2)
widget.layout().addWidget(self.progress_bar, 1, 0)
widget.layout().addWidget(self.btn_start, 1, 1)
self.show()
def start_jobs(self):
self.restart()
pool = QThreadPool.globalInstance()
for i in range(1, self.job_count+1):
worker = Worker(i)
worker.signals.completed.connect(self.complete)
worker.signals.started.connect(self.start)
pool.start(worker)
def restart(self):
self.progress_bar.setValue(0)
self.comleted_jobs = []
self.btn_start.setEnabled(False)
def start(self, n):
self.list.addItem(f'Job #{n} started...')
def complete(self, n):
self.list.addItem(f'Job #{n} completed.')
self.comleted_jobs.append(n)
self.progress_bar.setValue(len(self.comleted_jobs))
if len(self.comleted_jobs) == self.job_count:
self.btn_start.setEnabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec())
def complete(self, n):
self.list.addItem(f'Job #{n} completed.')
self.comleted_jobs.append(n)
self.progress_bar.setValue(len(self.comleted_jobs))
if len(self.comleted_jobs) == self.job_count:
self.btn_start.setEnabled(True)
Linguagem de código: Python ( python )
Saída:
Classe de sinais
Defina a classe Signals que herda da QObject
classe para suportar sinais. Na Signals
aula, definimos dois sinais:
- O
started
sinal será emitido quando um trabalhador for iniciado. - O
completed
sinal será emitido quando um trabalhador for concluído.
Ambos os sinais aceitam um número inteiro que identifica o número do trabalho:
class Signals(QObject):
started = Signal(int)
completed = Signal(int)
Linguagem de código: Python ( python )
Classe trabalhadora
A Worker
classe herda da QRunnable
classe. A Worker
classe representa uma tarefa de longa execução que transferimos para um thread de trabalho:
class Worker(QRunnable):
def __init__(self, n):
super().__init__()
self.n = n
self.signals = Signals()
@Slot()
def run(self):
self.signals.started.emit(self.n)
time.sleep(self.n*1.1)
self.signals.completed.emit(self.n)
Linguagem de código: Python ( python )
Primeiro, inicialize o número do trabalho (n) e o objeto Signals no __init__()
método.
Segundo, substitua o run()
método da QRunnable
classe. Para simular uma tarefa de longa duração, usamos a sleep()
função do módulo de tempo. Antes de iniciar o cronômetro, emitimos o sinal de início; após a conclusão do cronômetro, emitimos o sinal concluído.
Classe MainWindow
A MainWindow
classe define o UI
para o aplicativo:
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle('QThreadPool Demo')
self.comleted_jobs = []
self.job_count = 10
widget = QWidget()
widget.setLayout(QGridLayout())
self.setCentralWidget(widget)
self.btn_start = QPushButton('Start', clicked=self.start_jobs)
self.progress_bar = QProgressBar(minimum=0, maximum=self.job_count)
self.list = QListWidget()
widget.layout().addWidget(self.list, 0, 0, 1, 2)
widget.layout().addWidget(self.progress_bar, 1, 0)
widget.layout().addWidget(self.btn_start, 1, 1)
self.show()
def start_jobs(self):
self.restart()
pool = QThreadPool.globalInstance()
for i in range(1, self.job_count+1):
runnable = Worker(i)
runnable.signals.completed.connect(self.complete)
runnable.signals.started.connect(self.start)
pool.start(runnable)
def restart(self):
self.progress_bar.setValue(0)
self.comleted_jobs = []
self.btn_start.setEnabled(False)
def start(self, n):
self.list.addItem(f'Job #{n} started...')
def complete(self, n):
self.list.addItem(f'Job #{n} completed.')
self.comleted_jobs.append(n)
self.progress_bar.setValue(len(self.comleted_jobs))
if len(self.comleted_jobs) == self.job_count:
self.btn_start.setEnabled(True)
Linguagem de código: Python ( python )
Primeiro, inicialize o número de jobs ( job_count
) e completed_jobs
liste no __init__()
método da MainWindow
classe:
self.job_count = 10
self.comleted_jobs = []
Linguagem de código: Python ( python )
Segundo, defina o start_jobs()
método que será executado quando o usuário clicar no botão iniciar:
def start_jobs(self):
self.restart()
pool = QThreadPool.globalInstance()
for i in range(1, self.job_count+1):
worker = Worker(i)
worker.signals.completed.connect(self.complete)
worker.signals.started.connect(self.start)
pool.start(worker)
Linguagem de código: Python ( python )
O restart()
redefine o completed_jobs
, atualiza a barra de progresso para zero e desativa o botão Iniciar:
def restart(self):
self.progress_bar.setValue(0)
self.comleted_jobs = []
self.btn_start.setEnabled(False)
Linguagem de código: Python ( python )
Para obter o
objeto, usamos o QThreadPool
globalInstance()
da
classe:QThreadPool
pool = QThreadPool.globalInstance()
Linguagem de código: Python ( python )
Criamos vários trabalhadores, conectamos seus sinais aos métodos da MainWindow
classe e iniciamos threads de trabalho usando o start()
método do QThreadPool
objeto.
O start()
método adiciona a mensagem que inicia um thread de trabalho ao QListWidget
:
def start(self, n):
self.list.addItem(f'Job #{n} started...')
Linguagem de código: Python ( python )
O completed()
método é executado sempre que um thread de trabalho é concluído. Ele adiciona uma mensagem ao QListWidget
, atualiza a barra de progresso e ativa o botão Iniciar se todos os threads de trabalho forem concluídos:
def complete(self, n):
self.list.addItem(f'Job #{n} completed.')
self.comleted_jobs.append(n)
self.progress_bar.setValue(len(self.comleted_jobs))
if len(self.comleted_jobs) == self.job_count:
self.btn_start.setEnabled(True)
Linguagem de código: Python ( python )
Usando QThreadPool para obter preços de ações
O seguinte programa de listagem de ações lê símbolos de ações do symbols.txt
arquivo e usa QThreadPool
para obter os preços das ações do site Yahoo Finance:
Programa de listagem de ações:
import sys
from pathlib import Path
from PyQt6.QtCore import QRunnable, Qt, QObject, QThreadPool, pyqtSignal as Signal, pyqtSlot as Slot
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QGridLayout, QProgressBar, QTableWidget, QTableWidgetItem
from PyQt6.QtGui import QIcon
from lxml import html
import requests
class Signals(QObject):
completed = Signal(dict)
class Stock(QRunnable):
BASE_URL = 'https://finance.yahoo.com/quote/'
def __init__(self, symbol):
super().__init__()
self.symbol = symbol
self.signal = Signals()
@Slot()
def run(self):
stock_url = f'{self.BASE_URL}{self.symbol}'
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"}
response = requests.get(stock_url, headers=headers)
if response.status_code != 200:
self.signal.completed.emit({'symbol': self.symbol, 'price': 'N/A'})
return
tree = html.fromstring(response.text)
price_text = tree.xpath(
'//*[@id="quote-header-info"]/div[3]/div[1]/div[1]/fin-streamer[1]/text()'
)
if not price_text:
self.signal.completed.emit({'symbol': self.symbol, 'price': 'N/A'})
return
price = float(price_text[0].replace(',', ''))
self.signal.completed.emit({'symbol': self.symbol, 'price': price})
class Window(QMainWindow):
def __init__(self, filename, *args, **kwargs):
super().__init__(*args, **kwargs)
self.symbols = self.read_symbols(filename)
self.results = []
self.setWindowTitle('Stock Listing')
self.setGeometry(100, 100, 400, 300)
self.setWindowIcon(QIcon('./assets/stock.png'))
widget = QWidget()
widget.setLayout(QGridLayout())
self.setCentralWidget(widget)
# set up button & progress bar
self.btn_start = QPushButton('Get Prices', clicked=self.get_prices)
self.progress_bar = QProgressBar(minimum=1, maximum=len(self.symbols))
# set up table widget
self.table = QTableWidget(widget)
self.table.setColumnCount(2)
self.table.setColumnWidth(0, 150)
self.table.setColumnWidth(1, 150)
self.table.setHorizontalHeaderLabels(['Symbol', 'Price'])
widget.layout().addWidget(self.table, 0, 0, 1, 2)
widget.layout().addWidget(self.progress_bar, 1, 0)
widget.layout().addWidget(self.btn_start, 1, 1)
# show the window
self.show()
def read_symbols(self, filename):
"""
Read symbols from a file
"""
path = Path(filename)
text = path.read_text()
return [symbol.strip() for symbol in text.split('\n')]
def reset_ui(self):
self.progress_bar.setValue(1)
self.table.setRowCount(0)
def get_prices(self):
# reset ui
self.reset_ui()
# start worker threads
pool = QThreadPool.globalInstance()
stocks = [Stock(symbol) for symbol in self.symbols]
for stock in stocks:
stock.signal.completed.connect(self.update)
pool.start(stock)
def update(self, data):
# add a row to the table
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(data['symbol']))
self.table.setItem(row, 1, QTableWidgetItem(str(data['price'])))
# update the progress bar
self.progress_bar.setValue(row + 1)
# sort the list by symbols once completed
if row == len(self.symbols) - 1:
self.table.sortItems(0, Qt.SortOrder.AscendingOrder)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window('symbols.txt')
sys.exit(app.exec())
Linguagem de código: Python ( python )
Como funciona.
Classe de sinais
Definimos a Signals
classe que é uma subclasse do QObject
. A classe Signals possui uma variável de classe concluída que é uma instância da Signal
classe.
O sinal concluído contém um dicionário e é emitido assim que o programa conclui a obtenção do preço das ações.
class Signals(QObject):
completed = Signal(dict)
Linguagem de código: Python ( python )
Classe de ações
A STock
classe herda da QRunnable
classe. Ele substitui o run()
método que obtém o preço das ações do site do Yahoo Finance.
Depois de concluído, o run()
método emite o sinal concluído com o símbolo da ação e o preço.
Se ocorrer um erro como o símbolo não ser encontrado ou o site alterar a forma como exibe o preço da ação, o run()
método retornará o símbolo com o preço como uma string N/A.
class Stock(QRunnable):
BASE_URL = 'https://finance.yahoo.com/quote/'
def __init__(self, symbol):
super().__init__()
self.symbol = symbol
self.signal = Signals()
@Slot()
def run(self):
stock_url = f'{self.BASE_URL}{self.symbol}'
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"}
response = requests.get(stock_url, headers=headers)
if response.status_code != 200:
self.signal.completed.emit({'symbol': self.symbol, 'price': 'N/A'})
return
tree = html.fromstring(response.text)
price_text = tree.xpath(
'//*[@id="quote-header-info"]/div[3]/div[1]/div[1]/fin-streamer[1]/text()'
)
if not price_text:
self.signal.completed.emit({'symbol': self.symbol, 'price': 'N/A'})
return
price = float(price_text[0].replace(',', ''))
self.signal.completed.emit({'symbol': self.symbol, 'price': price})
Linguagem de código: Python ( python )
Observe que o Yahoo Finance pode alterar sua estrutura. Para que o programa funcione, você precisa alterar o XPath do preço do novo:
//*[@id="quote-header-info"]/div[3]/div[1]/div[1]/fin-streamer[1]/text()
Linguagem de código: Python ( python )
Classe MainWindow
Primeiro, leia os símbolos de um arquivo e atribua-os às self.symbols
variáveis:
self.symbols = self.read_symbols(filename)
Linguagem de código: Python ( python )
O read_symbols()
método é assim:
def read_symbols(self, filename):
path = Path(filename)
text = path.read_text()
return [symbol.strip() for symbol in text.split('\n')]
Linguagem de código: Python ( python )
O arquivo de texto ( symbols.txt
) contém cada símbolo por linha:
AAPL
MSFT
GOOG
AMZN
TSLA
META
NVDA
BABA
CRM
INTC
PYPL
AMD
ATVI
EA
TTD
ORCL
Linguagem de código: Python ( python )
Segundo, defina o get_prices
que usa QThreadPool
para criar threads de trabalho para obter preços de ações:
def get_prices(self):
# reset ui
self.reset_ui()
# start worker threads
pool = QThreadPool.globalInstance()
stocks = [Stock(symbol) for symbol in self.symbols]
for stock in stocks:
stock.signal.completed.connect(self.update)
pool.start(stock)
Linguagem de código: Python ( python )
O reset_ui()
método limpa todas as linhas QTableWidget
e define a barra de progresso para seu valor mínimo:
def reset_ui(self):
self.table.setRowCount(0)
self.progress_bar.setValue(1)
Linguagem de código: Python ( python )
Terceiro, defina o
método que será chamado assim que cada thread de trabalho for concluído. O update()
método adiciona uma nova linha à tabela, atualiza a barra de progresso e classifica os símbolos quando todos os threads de trabalho são concluídos:update()
def update(self, data):
# add a row to the table
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(data['symbol']))
self.table.setItem(row, 1, QTableWidgetItem(str(data['price'])))
# update the progress bar
self.progress_bar.setValue(row + 1)
# sort the list by symbols once completed
if row == len(self.symbols) - 1:
self.table.sortItems(0, Qt.SortOrder.AscendingOrder)
Linguagem de código: Python ( python )
Resumo
- Use a
QRunnable
classe para representar uma tarefa de longa execução que será transferida para um thread de trabalho. - Use a
QThreadPool
classe para gerenciar threads de trabalho automaticamente. - Cada aplicativo PyQt possui um
objeto. Use oQThreadPool
globalInstance()
método para obter o
objeto global.QThreadPool
- Use o
start()
método doQThreadPool
objeto para iniciar um thread de trabalho.