在访问 SQLite3 数据库连接时关注竞争条件,该连接是在 QThread 内的 Pynput 侦听器调用的线程中访问的
Concerned about race conditions while accessing SQLite3 database connection that's accessed in thread invoked by Pynput listener inside a QThread
我正在使用 Pyside2 编写 Windows 应用程序。由于我使用多线程的方式的性质,我不得不在多个线程中与同一个 Sqlite3 数据库进行交互。我创建了一个 <100 行 Minimal, complete, verifiable example 几乎完全相同地复制了这个问题。
问题: 我目前正在使用 pynput module 在按下按钮后在后台监视键 activity,而 Qt对于 "j" + "k" 的热键组合,GUI 不在焦点范围内。一旦按下热键组合,就会截取屏幕截图,通过 OCR 处理图像并与 OCR 文本一起保存到数据库中。图像路径通过一系列连接信号发送到主 GUI 线程。关键监控发生在另一个 QThread
以防止关键监控和图像处理影响来自 运行 的主 Qt 事件循环。一旦 QThread 启动并发出它的开始信号,我在 key_monitor 实例中调用 monitor_for_hot_key_combo
函数,该实例将 listener
实例化为 threading.Thread
,它被分配给 key_monitor 成员函数 on_release
和 on_press
作为回调,每次按下键时调用。
问题就出在这里。这些回调与 image_process
class 的 imageprocessing_obj
实例在与实例化 class 不同的线程中交互。因此,当 image_process
成员函数交互时使用 SQlite 数据库时,它们在一个单独的线程中执行此操作,而不是在创建数据库连接的线程中进行。Now, SQLite "can be safely used by multiple threads 前提是没有同时使用单个数据库连接 两个或多个线程”。为了让你
必须将 sqlite3.connect()
的 check_same_thread
参数设置为 False。但是,如果可能的话,我宁愿避免这种数据库的多线程访问,以防止未定义的行为。
可能的解决方案:我一直在想是否这两个线程,threading.Thread
和QThread
都不是必需的,它们都可以在 Pynput 线程中完成。但是,我似乎无法弄清楚如何在仍然能够将信号发送回主 Qt 事件循环的同时仅使用 Pynput 线程。
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)
首先一个QThread
不是一个Qt线程,也就是说,它不是一个新的线程类型,QThread
是一个class 管理每个平台的本机线程。因此处理 QThread
的线程具有与 threading.Thread
.
相同的特征
另一方面,在 GUI 中使用线程的目的不是阻塞称为 GUI 线程的主线程,在您的 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_())
我正在使用 Pyside2 编写 Windows 应用程序。由于我使用多线程的方式的性质,我不得不在多个线程中与同一个 Sqlite3 数据库进行交互。我创建了一个 <100 行 Minimal, complete, verifiable example 几乎完全相同地复制了这个问题。
问题: 我目前正在使用 pynput module 在按下按钮后在后台监视键 activity,而 Qt对于 "j" + "k" 的热键组合,GUI 不在焦点范围内。一旦按下热键组合,就会截取屏幕截图,通过 OCR 处理图像并与 OCR 文本一起保存到数据库中。图像路径通过一系列连接信号发送到主 GUI 线程。关键监控发生在另一个 QThread
以防止关键监控和图像处理影响来自 运行 的主 Qt 事件循环。一旦 QThread 启动并发出它的开始信号,我在 key_monitor 实例中调用 monitor_for_hot_key_combo
函数,该实例将 listener
实例化为 threading.Thread
,它被分配给 key_monitor 成员函数 on_release
和 on_press
作为回调,每次按下键时调用。
问题就出在这里。这些回调与 image_process
class 的 imageprocessing_obj
实例在与实例化 class 不同的线程中交互。因此,当 image_process
成员函数交互时使用 SQlite 数据库时,它们在一个单独的线程中执行此操作,而不是在创建数据库连接的线程中进行。Now, SQLite "can be safely used by multiple threads 前提是没有同时使用单个数据库连接 两个或多个线程”。为了让你
必须将 sqlite3.connect()
的 check_same_thread
参数设置为 False。但是,如果可能的话,我宁愿避免这种数据库的多线程访问,以防止未定义的行为。
可能的解决方案:我一直在想是否这两个线程,threading.Thread
和QThread
都不是必需的,它们都可以在 Pynput 线程中完成。但是,我似乎无法弄清楚如何在仍然能够将信号发送回主 Qt 事件循环的同时仅使用 Pynput 线程。
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)
首先一个QThread
不是一个Qt线程,也就是说,它不是一个新的线程类型,QThread
是一个class 管理每个平台的本机线程。因此处理 QThread
的线程具有与 threading.Thread
.
另一方面,在 GUI 中使用线程的目的不是阻塞称为 GUI 线程的主线程,在您的 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_())