Preocupado com as condições de corrida ao acessar a conexão com o banco de dados SQLite3 acessada no encadeamento invocado pelo ouvinte Pynput dentro de um QThread
Estou escrevendo um aplicativo do Windows com o Pyside2. Devido à natureza de como estou usando o multithreading, estou tendo que interagir com o mesmo banco de dados Sqlite3 em vários threads. Eu criei uma linha <100Exemplo mínimo, completo e verificável que quase idêntica replica o problema.
O problema: Atualmente, estou usando omódulo pynput monitorar a atividade das teclas em segundo plano depois que o PushButton for pressionado, enquanto a GUI Qt estiver fora de foco para uma combinação de teclas de atalho de "j" + "k". Depois que a combinação de teclas de atalho é pressionada, uma captura de tela é feita, a imagem é processada via OCR e salva em um banco de dados junto com o texto do OCR. O caminho da imagem é enviado através de uma série de sinais conectados ao thread da GUI principal. O monitoramento chave acontece em outroQThread
para impedir que o monitoramento de chaves e o processamento de imagens afetem o loop de eventos Qt principal. Depois que o QThread inicia e emite seu sinal de início, chamo omonitor_for_hot_key_combo
função na instância key_monitor que instancialistener
como umthreading.Thread
, às quais são atribuídas funções de membro key_monitoron_release
eon_press
como retornos de chamada, que são chamados toda vez que uma tecla é pressionada.
É aqui que está o problema. Esses retornos de chamada interagem com oimageprocessing_obj
instância doimage_process
classe em um segmento diferente do qual a classe foi instanciada. Portanto, quandoimage_process
Como as funções membro são interagidas com o uso do banco de dados SQlite, elas o fazem em um encadeamento separado em que a conexão com o banco de dados foi criada.Agora, o SQLite "pode ser usado com segurança por vários threads providenciou quenenhuma conexão única com o banco de dados é usada simultaneamente em dois ou mais threads ". Para permitir isso, você deve definir ocheck_same_thread
argumento parasqlite3.connect()
para Falso. No entanto, eu preferiria evitar esse acesso multithread do banco de dados, se possível, para evitar um comportamento indefinido.
A solução possível: Fiquei me perguntando se os dois threads, ambosthreading.Thread
eQThread
não são necessários e tudo pode ser feito no segmento Pynput. No entanto, parece que não consigo descobrir como usar o thread Pynput enquanto ainda é capaz de enviar sinais de volta ao loop de eventos Qt principal.
qtui.py
from PySide2 import QtCore, QtWidgets
from PySide2.QtCore import *
import HotKeyMonitor
class Ui_Form(object):
def __init__(self):
self.worker = None
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(400, 300)
self.pressbutton = QtWidgets.QPushButton(Form)
self.pressbutton.setObjectName("PushButton")
self.pressbutton.clicked.connect(self.RunKeyMonitor)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1))
self.pressbutton.setText(QtWidgets.QApplication.translate("Form", "Press me", None, -1))
def RunKeyMonitor(self):
self.Thread_obj = QThread()
self.HotKeyMonitor_Obj = HotKeyMonitor.key_monitor()
self.HotKeyMonitor_Obj.moveToThread(self.Thread_obj)
self.HotKeyMonitor_Obj.image_processed_km.connect(self.print_OCR_result)
self.Thread_obj.started.connect(self.HotKeyMonitor_Obj.monitor_for_hotkey_combo)
self.Thread_obj.start()
def print_OCR_result(self, x):
print("Slot being called to print image path string")
print(x)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
sys.exit(app.exec_())
HotKeyMonitor.py
from pynput import keyboard
from PySide2.QtCore import QObject, Signal
import imageprocess
class key_monitor(QObject):
image_processed_km = Signal(str)
def __init__(self):
super().__init__()
self.prev_key = None
self.listener = None
self.imageprocessing_obj = imageprocess.image_process()
self.imageprocessing_obj.image_processed.connect(self.image_processed_km.emit)
def on_press(self,key):
pass
def on_release(self,key):
if type(key) == keyboard._win32.KeyCode:
if key.char.lower() == "j":
self.prev_key = key.char.lower()
elif key.char.lower() == "k" and self.prev_key == "j":
print("key combination j+k pressed")
self.prev_key = None
self.imageprocessing_obj.process_image()
else:
self.prev_key = None
def stop_monitoring(self):
self.listener.stop()
def monitor_for_hotkey_combo(self):
with keyboard.Listener(on_press=self.on_press, on_release = self.on_release) as self.listener:self.listener.join()
imageprocess.py
import uuid,os,sqlite3,pytesseract
from PIL import ImageGrab
from PySide2.QtCore import QObject, Signal
class image_process(QObject):
image_processed = Signal(str)
def __init__(self):
super().__init__()
self.screenshot = None
self.db_connection = sqlite3.connect("testdababase.db", check_same_thread=False)
self.cursor = self.db_connection.cursor()
self.cursor.execute("CREATE TABLE IF NOT EXISTS testdb (OCRstring text, filepath text)")
def process_image(self):
self.screenshot = ImageGrab.grab()
self.screenshot_path = os.getcwd() + "\\" + uuid.uuid4().hex + ".jpg"
self.screenshot.save(self.screenshot_path )
self.ocr_string = pytesseract.image_to_string(self.screenshot)
self.cursor.execute("INSERT INTO testdb (OCRstring, filepath) VALUES (?,?)",(self.ocr_string, self.screenshot_path))
self.image_processed.emit(self.screenshot_path)