reocupado por las condiciones de carrera al acceder a la conexión de base de datos SQLite3 a la que se accede en el hilo invocado por Pynput listener dentro de un QThread
Estoy escribiendo una aplicación de Windows con Pyside2. Debido a la naturaleza de cómo estoy usando subprocesos múltiples, tengo que interactuar con la misma base de datos Sqlite3 en múltiples subprocesos. He creado una línea <1 Ejemplo mínimo, completo y verificable que casi idénticamente replica el problema.
El problema Actualmente estoy usando lapynput module para monitorear la actividad clave en segundo plano una vez que se ha presionado el PushButton, mientras que la GUI Qt está fuera de foco para una combinación de teclas de acceso rápido de "j" + "k". Una vez que se presiona la combinación de teclas de acceso rápido, se toma una captura de pantalla, la imagen se procesa a través de OCR y se guarda en una base de datos junto con el texto de OCR. La ruta de la imagen se envía a través de una serie de señales conectadas al hilo principal de la GUI. El monitoreo clave ocurre en otraQThread
para evitar que el monitoreo clave y el procesamiento de imágenes afecten la ejecución del bucle principal de eventos Qt. Una vez que el QThread se inicia y emite su señal de inicio, llamo amonitor_for_hot_key_combo
unción @ en la instancia de key_monitor que instancialistener
como unthreading.Thread
, que tiene asignadas funciones miembro key_monitoron_release
yon_press
como devoluciones de llamada, que se llaman cada vez que se presiona una tecla.
Aquí es donde radica el problema. Esas devoluciones de llamada interactúan con laimageprocessing_obj
instancia de laimage_process
class en un hilo diferente al que se instancia la clase. Por lo tanto, cuandoimage_process
las funciones miembro interactúan con las que usan la base de datos SQlite, lo hacen en un subproceso diferente al que se creó la conexión de la base de datos. Ahora, SQLite "puede ser usado de manera segura por múltiples hilos siempre queno se utiliza una conexión de base de datos única simultáneamen en dos o más hilos ". Para permitir esto, debe configurar elcheck_same_thread
argumento parasqlite3.connect()
a falso. Sin embargo, preferiría evitar este acceso multiproceso de la base de datos si es posible para evitar un comportamiento indefinido.
La solución posible: Me he estado preguntando si los dos hilos, ambosthreading.Thread
yQThread
no son necesarios y todo se puede hacer dentro del hilo de Pynput. Sin embargo, parece que no puedo entender cómo usar el hilo de Pynput sin dejar de enviar señales al bucle principal de eventos Qt.
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)