Обеспокоен условиями гонки при доступе к соединению с базой данных SQLite3, доступ к которому осуществляется в потоке, вызываемом прослушивателем Pynput внутри QThread.

Я пишу приложение Windows с Pyside2. Из-за того, как я использую многопоточность, мне приходится взаимодействовать с одной и той же базой данных Sqlite3 в нескольких потоках. Я создал ‹100-строчный минимальный, полный, проверяемый пример, который почти идентично воспроизводит проблему.

Проблема: в настоящее время я использую модуль pynput для мониторинга ключа активность в фоновом режиме после нажатия кнопки PushButton, в то время как графический интерфейс Qt не в фокусе для комбинации горячих клавиш «j» + «k». После нажатия комбинации горячих клавиш делается снимок экрана, изображение обрабатывается с помощью OCR и сохраняется в базе данных вместе с текстом OCR. Путь изображения отправляется через ряд подключенных сигналов в основной поток графического интерфейса. Мониторинг ключей происходит в другом QThread, чтобы предотвратить влияние мониторинга ключей и обработки изображений на выполнение основного цикла событий Qt. Как только QThread запускается и выдает сигнал запуска, я вызываю функцию monitor_for_hot_key_combo в экземпляре key_monitor, который создает экземпляр listener как threading.Thread, которому назначаются функции-члены key_monitor on_release и on_press в качестве обратных вызовов, которые вызываются каждый раз при нажатии клавиши.

Вот в чем проблема. Эти обратные вызовы взаимодействуют с экземпляром imageprocessing_obj класса image_process в потоке, отличном от того, в котором был создан экземпляр класса. Поэтому, когда взаимодействуют функции-члены image_process, использующие базу данных SQlite, они делают это в отдельном потоке, чем соединение с базой данных, созданное в . Теперь SQLite "может безопасно использоваться несколькими потоками при условии, что ни одна база данных соединение используется одновременно в двух или более потоках». Чтобы разрешить это, вы должны установить аргумент check_same_thread для sqlite3.connect() в False. Тем не менее, я предпочитаю избегать многопоточного доступа к базе данных, если это возможно, чтобы предотвратить неопределенное поведение.

Возможное решение: мне интересно, не нужны ли два потока, threading.Thread и QThread, и все это можно сделать в потоке Pynput. Однако я не могу понять, как просто использовать поток Pynput, сохраняя при этом возможность отправлять сигналы обратно в основной цикл событий 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)

person Mrcitrusboots    schedule 10.07.2018    source источник
comment
вы говорите одна и та же база данных Sqlite3 в нескольких потоках, но я вижу только один поток, а где остальные?   -  person eyllanesc    schedule 10.07.2018
comment
@eyllanescв документации pynput упоминается, что листнер наследуется от threading.thread Источник: pypi.org/project/pynput Все обратные вызовы, упомянутые в экземпляре прослушивателя, вызываются из threading.Thread , как указано в документации. Поэтому у меня есть Qthread, который вы видите в основном модуле qtgui.py, и threading.thread, который вы видите в модуле hotkeymonitor.py.   -  person Mrcitrusboots    schedule 10.07.2018
comment
хорошо, я понимаю, но тогда почему он создает еще один поток, чтобы pyinput уже работал в потоке и не блокировал графический интерфейс?   -  person eyllanesc    schedule 10.07.2018
comment
@eyllanesc Вот в чем моя проблема. Поскольку слушатель pynput по умолчанию использует threading.Thread, я ищу способ автоматически использовать Qthread, который я уже создал для слушателя, чтобы избежать многопоточного доступа к базе данных SQlite.   -  person Mrcitrusboots    schedule 10.07.2018
comment
В графическом интерфейсе цель использования потоков состоит не в том, чтобы заблокировать основной поток, называемый потоком графического интерфейса, в вашем случае я вижу 2 задачи блокировки, первая — pyinput, но она уже выполняется в потоке, поэтому с этим проблем нет, а другой - это OCR, который должен выполняться в другом потоке, доступ к БД не блокируется, не потребляет никаких ресурсов, поэтому его можно выполнять в графическом интерфейсе.   -  person eyllanesc    schedule 10.07.2018
comment
Мое решение сработало для вас?   -  person eyllanesc    schedule 11.07.2018
comment
@eyllanesc Да, это прекрасно работает! Мне нравится, как вы избегаете многопоточного доступа к базе данных, перенося взаимодействия с базой данных в основной поток.   -  person Mrcitrusboots    schedule 12.07.2018


Ответы (1)


Во-первых, QThread — это не поток Qt, то есть это не новый тип потока, а QThread — это класс, который управляет собственными потоками каждой платформы. поэтому поток, обрабатывающий QThread, имеет те же характеристики, что и threading.Thread.

С другой стороны, цель использования потоков в графическом интерфейсе состоит не в том, чтобы заблокировать основной поток, называемый потоком графического интерфейса, в вашем pynput у него уже есть свой поток, поэтому проблем не возникнет. Другая блокирующая задача — это OCR, поэтому мы должны выполнить ее в новом потоке. Задача базы не дорогая, поэтому создавать поток не нужно.

keymonitor.py

from pynput import keyboard
import time
from PySide2 import QtCore

class KeyMonitor(QtCore.QObject):
    letterPressed = QtCore.Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.listener = keyboard.Listener(on_release = self.on_release)

    def on_release(self,key):
        if type(key) == keyboard._win32.KeyCode:
            self.letterPressed.emit(key.char.lower())

    def stop_monitoring(self):
        self.listener.stop()

    def start_monitoring(self):
        self.listener.start()

imageprocess.py

import uuid
import pytesseract

from PIL import ImageGrab

from PySide2 import QtCore

class ProcessWorker(QtCore.QObject):
    processSignal = QtCore.Signal(str, str)

    def doProcess(self):
        screenshot = ImageGrab.grab()
        screenshot_path =  QtCore.QDir.current().absoluteFilePath(uuid.uuid4().hex+".jpg")
        screenshot.save(screenshot_path )
        print("start ocr")
        ocr_string = pytesseract.image_to_string(screenshot)
        print(ocr_string, screenshot_path)
        self.processSignal.emit(ocr_string, screenshot_path)
        self.thread().quit()

main.py

from keymonitor import KeyMonitor
from imageprocess import ProcessWorker
from PySide2 import QtCore, QtWidgets

import sqlite3

class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.last_letter = ""
        self.current_letter = ""

        lay = QtWidgets.QVBoxLayout(self)
        button = QtWidgets.QPushButton("Start")
        button.clicked.connect(self.onClicked)
        lay.addWidget(button)

        self.keymonitor = KeyMonitor()
        self.keymonitor.letterPressed.connect(self.onLetterPressed)

        self.db_connection = sqlite3.connect("testdababase.db")
        self.cursor = self.db_connection.cursor()
        self.cursor.execute("CREATE TABLE IF NOT EXISTS testdb (OCRstring text, filepath text)")
        self.threads = []

    def onClicked(self):
        self.keymonitor.start_monitoring()

    def onLetterPressed(self, letter):
        if self.last_letter:
            if self.current_letter:
                self.last_letter = self.current_letter
            self.current_letter = letter
        else:
            self.last_letter = letter

        if self.last_letter == "j" and self.current_letter == "k":
            print("j+k")
            self.start_processing()

    def start_processing(self):
        thread = QtCore.QThread()
        self.worker = ProcessWorker()
        self.worker.processSignal.connect(self.onProcessSignal)
        self.worker.moveToThread(thread)
        thread.started.connect(self.worker.doProcess)
        thread.finished.connect(self.worker.deleteLater)
        thread.finished.connect(lambda th=thread: self.threads.remove(th))
        thread.start()
        self.threads.append(thread)

    def onProcessSignal(self, ocr, path):
        print(ocr, path)
        self.cursor.execute("INSERT INTO testdb (OCRstring, filepath) VALUES (?,?)",(ocr, path))
        self.db_connection.commit()

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())
person eyllanesc    schedule 10.07.2018