PyQt多线程编程:创建线程服务器与管理二级线程
1. PyQt多线程基础
PyQt提供了一系列支持多线程编程的类,如
QMutex
、
QReadWriteLock
和
QSemaphore
。此外,PyQt应用程序还可以使用信号 - 槽机制在不同线程之间进行通信,这种机制既方便又实用。
2. 创建线程服务器
与其他一些GUI库不同,PyQt的网络套接字类与事件循环集成在一起。这意味着即使在单线程的PyQt应用程序中,用户界面在网络处理过程中也能保持响应。但如果需要处理多个同时的传入连接,使用多线程服务器会是更好的选择。
创建多线程服务器并不比创建单线程服务器复杂,二者的区别在于:单线程服务器为每个传入连接创建一个单独的套接字,而多线程服务器为每个新连接创建一个新线程,并在每个新线程内创建一个新套接字。
以下是一个完整的线程服务器示例代码:
class TcpServer(QTcpServer):
def __init__(self, parent=None):
super(TcpServer, self).__init__(parent)
def incomingConnection(self, socketId):
thread = Thread(socketId, self)
self.connect(thread, SIGNAL("finished()"),
thread, SLOT("deleteLater()"))
thread.start()
incomingConnection()
方法是从
QTcpServer
基类重新实现的,每当有新的网络连接到服务器时就会调用该方法。信号 - 槽连接用于确保线程在不再需要时被删除,从而使服务器的内存占用尽可能小。虽然必须在
QThread
子类中重新实现
QThread.run()
方法,但线程总是通过调用
QThread.start()
来启动(而不是直接调用
run()
)。
Thread
子类有一个静态变量和四个方法,其中
sendReply()
和
sendError()
方法与前一章所示的方法相同,这里省略。
class Thread(QThread):
lock = QReadWriteLock()
def __init__(self, socketId, parent):
super(Thread, self).__init__(parent)
self.socketId = socketId
def run(self):
socket = QTcpSocket()
if not socket.setSocketDescriptor(self.socketId):
self.emit(SIGNAL("error(int)"), socket.error())
return
while socket.state() == QAbstractSocket.ConnectedState:
nextBlockSize = 0
stream = QDataStream(socket)
stream.setVersion(QDataStream.Qt_4_2)
while True:
socket.waitForReadyRead(-1)
if socket.bytesAvailable() >= SIZEOF_UINT16:
nextBlockSize = stream.readUInt16()
break
if socket.bytesAvailable() < nextBlockSize:
while True:
socket.waitForReadyRead(-1)
if socket.bytesAvailable() >= nextBlockSize:
break
action = QString()
room = QString()
date = QDate()
stream >> action
if action in ("BOOK", "UNBOOK"):
stream >> room >> date
try:
Thread.lock.lockForRead()
bookings = Bookings.get(date.toPyDate())
finally:
Thread.lock.unlock()
uroom = unicode(room)
if action == "BOOK":
newlist = False
try:
Thread.lock.lockForRead()
if bookings is None:
newlist = True
finally:
Thread.lock.unlock()
if newlist:
try:
Thread.lock.lockForWrite()
bookings = Bookings[date.toPyDate()]
finally:
Thread.lock.unlock()
error = None
insert = False
try:
Thread.lock.lockForRead()
if len(bookings) < MAX_BOOKINGS_PER_DAY:
if uroom in bookings:
error = "Cannot accept duplicate booking"
else:
insert = True
else:
error = QString("%1 is fully booked").arg(
date.toString(Qt.ISODate))
finally:
Thread.lock.unlock()
if insert:
try:
Thread.lock.lockForWrite()
bisect.insort(bookings, uroom)
finally:
Thread.lock.unlock()
self.sendReply(socket, action, room, date)
else:
self.sendError(socket, error)
elif action == "UNBOOK":
error = None
remove = False
try:
Thread.lock.lockForRead()
if bookings is None or uroom not in bookings:
error = "Cannot unbook nonexistent booking"
else:
remove = True
finally:
Thread.lock.unlock()
if remove:
try:
Thread.lock.lockForWrite()
bookings.remove(uroom)
finally:
Thread.lock.unlock()
self.sendReply(socket, action, room, date)
else:
self.sendError(socket, error)
else:
self.sendError(socket, "Unrecognized request")
socket.waitForDisconnected()
在
run()
方法中,首先创建一个新套接字并设置其套接字描述符。只要套接字处于连接状态,就可以使用它来接收请求并发送响应。这里使用
waitForReadyRead()
方法阻塞,直到有数据可用。
对于共享数据的访问,使用
QReadWriteLock
进行保护。当只需要读取数据时,使用
lockForRead()
;当需要写入数据时,使用
lockForWrite()
。使用
try...finally
块确保锁在使用后被解锁。
3. 线程同步机制
-
互斥锁(Mutex)
:由
QMutex类提供,也称为二进制信号量。互斥锁只允许锁定它的线程访问受保护的资源。 -
读写锁(Read/Write Lock)
:由
QReadWriteLock类提供,比互斥锁更细粒度。当只有读锁生效时,所有线程都可以同时读取数据;当有写锁生效时,其他线程会被阻塞。
以下是线程同步机制的比较表格:
| 同步机制 | 特点 | 适用场景 |
| ---- | ---- | ---- |
| 互斥锁(QMutex) | 只允许一个线程访问资源 | 对资源的读写操作都需要独占访问时 |
| 读写锁(QReadWriteLock) | 允许多个线程同时读,写时独占 | 读操作频繁,写操作较少的场景 |
4. 创建和管理二级线程
在GUI应用程序中,一个常见的用例是将处理任务传递给二级线程,以便用户界面保持响应并显示二级线程的进度。这里以Page Indexer应用程序为例,该应用程序对指定目录及其所有子目录中的HTML文件进行索引。
索引工作由二级线程完成,该线程与主线程通信,通知主线程已完成的进度以及索引何时完成。
以下是
Form
类的部分代码:
class Form(QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
self.fileCount = 0
self.filenamesForWords = collections.defaultdict(set)
self.commonWords = set()
self.lock = QReadWriteLock()
self.path = QDir.homePath()
self.walker = walker.Walker(self.lock, self)
self.connect(self.walker, SIGNAL("indexed(QString)"),
self.indexed)
self.connect(self.walker, SIGNAL("finished(bool)"),
self.finished)
self.connect(self.pathButton, SIGNAL("clicked()"),
self.setPath)
self.connect(self.findEdit, SIGNAL("returnPressed()"),
self.find)
fileCount
变量用于跟踪到目前为止已索引的文件数量,
filenamesForWords
默认字典的键是单词,值是文件名集合,
commonWords
集合保存出现至少250次的单词。
read/write
锁用于保护对
filenamesForWords
字典和
commonWords
集合的访问。
当用户点击“Set Path”按钮时,调用
setPath()
方法:
def setPath(self):
self.pathButton.setEnabled(False)
if self.walker.isRunning():
self.walker.stop()
self.walker.wait()
path = QFileDialog.getExistingDirectory(self,
"Choose a Path to Index", self.path)
if path.isEmpty():
self.statusLabel.setText("Click the 'Set Path' "
"button to start indexing")
self.pathButton.setEnabled(True)
return
self.path = QDir.toNativeSeparators(path)
self.findEdit.setFocus()
self.pathLabel.setText(self.path)
self.statusLabel.clear()
self.filesListWidget.clear()
self.fileCount = 0
self.filenamesForWords = collections.defaultdict(set)
self.commonWords = set()
self.walker.initialize(unicode(self.path),
self.filenamesForWords, self.commonWords)
self.walker.start()
具体操作步骤如下:
1. 禁用“Set Path”按钮。
2. 如果线程正在运行,停止线程并等待其完成。
3. 获取用户选择的路径。
4. 如果用户取消选择,恢复按钮状态并返回。
5. 设置路径并更新用户界面。
6. 初始化
walker
线程并启动它。
当
walker
线程完成一个文件的索引时,会发出
indexed()
信号,连接到
Form.indexed()
方法:
def indexed(self, fname):
self.statusLabel.setText(fname)
self.fileCount += 1
if self.fileCount % 25 == 0:
self.filesIndexedLCD.display(self.fileCount)
try:
self.lock.lockForRead()
indexedWordCount = len(self.filenamesForWords)
commonWordCount = len(self.commonWords)
finally:
self.lock.unlock()
self.wordsIndexedLCD.display(indexedWordCount)
self.commonWordsLCD.display(commonWordCount)
elif self.fileCount % 101 == 0:
self.commonWordsListWidget.clear()
try:
self.lock.lockForRead()
words = self.commonWords.copy()
finally:
self.lock.unlock()
self.commonWordsListWidget.addItems(sorted(words))
操作步骤如下:
1. 更新状态标签以显示刚索引的文件名称。
2. 每25个文件更新一次文件计数、已索引单词数和常用单词数的LCD显示。
3. 每101个文件更新一次常用单词列表。
当线程停止或完成时,会发出
finished()
信号,连接到
Form.finished()
方法:
def finished(self, completed):
self.statusLabel.setText("Indexing complete" \
if completed else "Stopped")
self.finishedIndexing()
finishedIndexing()
方法用于更新用户界面:
def finishedIndexing(self):
self.walker.wait()
self.filesIndexedLCD.display(self.fileCount)
self.wordsIndexedLCD.display(len(self.filenamesForWords))
self.commonWordsLCD.display(len(self.commonWords))
self.pathButton.setEnabled(True)
操作步骤如下:
1. 等待线程完成。
2. 更新用户界面上的显示信息。
3. 启用“Set Path”按钮。
5. 使用上下文管理器解锁
Python 2.6(以及带有适当
from __future__
语句的Python 2.5)提供了一种更简洁的语法,可以使用
with
关键字结合上下文管理器来替代
try...finally
块。
以下是使用上下文管理器解锁的示例代码:
class ReadLocker:
def __init__(self, lock):
self.lock = lock
def __enter__(self):
self.lock.lockForRead()
def __exit__(self, type, value, tb):
self.lock.unlock()
# 使用自定义的ReadLocker
with ReadLocker(self.lock):
found = word in self.commonWords
# 使用PyQt自带的QReadLocker
with QReadLocker(self.lock):
found = word in self.commonWords
使用上下文管理器的好处是代码更简洁,并且能确保锁在使用后被正确解锁。
6. 用户交互和搜索功能
在索引过程中,用户可以随时与用户界面进行交互,不会出现冻结或性能下降的情况。用户可以在查找编辑框中输入文本并按Enter键,以填充包含他们输入单词的文件列表。
以下是
find()
方法的代码:
def find(self):
word = unicode(self.findEdit.text())
if not word:
self.statusLabel.setText("Enter a word to find in files")
return
self.statusLabel.clear()
self.filesListWidget.clear()
word = word.lower()
if " " in word:
word = word.split()[0]
try:
self.lock.lockForRead()
found = word in self.commonWords
finally:
self.lock.unlock()
if found:
self.statusLabel.setText(
"Common words like '%s' are not indexed" % word)
return
try:
self.lock.lockForRead()
files = self.filenamesForWords.get(word, set()).copy()
finally:
self.lock.unlock()
if not files:
self.statusLabel.setText(
"No indexed file contains the word '%s'" % word)
return
files = [QDir.toNativeSeparators(name) for name in \
sorted(files, key=unicode.lower)]
self.filesListWidget.addItems(files)
self.statusLabel.setText(
"%d indexed files contain the word '%s'" % (
len(files), word))
操作步骤如下:
1. 获取用户输入的单词。
2. 如果单词为空,提示用户输入单词。
3. 清除状态标签和文件列表。
4. 将单词转换为小写并处理包含空格的情况。
5. 检查单词是否为常用单词,如果是,给出提示并返回。
6. 从
filenamesForWords
字典中获取包含该单词的文件列表。
7. 如果没有匹配的文件,给出提示并返回。
8. 对文件列表进行排序并添加到文件列表小部件中。
9. 更新状态标签显示匹配的文件数量。
7. 应用程序的终止处理
当用户按下Esc键时,会调用
reject()
方法:
def reject(self):
if self.walker.isRunning():
self.walker.stop()
self.finishedIndexing()
else:
self.accept()
当应用程序终止时,无论是通过
reject()
方法中的
accept()
调用,还是其他方式(如用户点击关闭按钮),都会调用
closeEvent()
方法:
def closeEvent(self, event=None):
self.walker.stop()
self.walker.wait()
操作步骤如下:
1. 如果索引正在进行,停止线程并等待其完成。
2. 更新用户界面。
3. 确保线程停止并完成,以实现干净的终止。
通过以上内容,我们详细介绍了如何使用PyQt创建线程服务器和管理二级线程,以及如何处理线程同步和用户交互。这些技术可以帮助我们开发出更高效、响应更灵敏的GUI应用程序。
PyQt多线程编程:创建线程服务器与管理二级线程
8. 多线程通信机制总结
在PyQt多线程编程中,主要有三种通信机制:信号 - 槽机制、方法调用和共享数据。下面通过一个表格来总结这三种机制的特点和适用场景:
| 通信机制 | 特点 | 适用场景 |
| ---- | ---- | ---- |
| 信号 - 槽机制 | 异步通信,不阻塞线程,可跨线程传递信息 | 二级线程向主线程传递状态信息,如文件索引进度等 |
| 方法调用 | 同步通信,主线程调用二级线程方法控制其行为 | 主线程控制二级线程的启动、停止等操作 |
| 共享数据 | 多个线程可访问同一数据,但需进行同步保护 | 多个线程共享数据,如文件索引结果、常用单词集合等 |
下面是一个mermaid流程图,展示了这三种通信机制在Page Indexer应用程序中的交互过程:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A([主线程]):::startend -->|方法调用| B(启动二级线程):::process
B -->|信号 - 槽| C(通知主线程进度):::process
C --> A
D(共享数据: 文件索引结果、常用单词集合):::process
A <-->|读写操作| D
B <-->|读写操作| D
9. 线程安全与重入性
在多线程编程中,线程安全和重入性是两个重要的概念:
-
线程安全
:指多个线程可以同时调用一个对象的方法,并且底层系统(如PyQt)会自动序列化对共享数据的访问。例如,
QReadWriteLock
就是线程安全的,多个线程可以同时对其进行操作,系统会保证数据的一致性。
-
重入性
:重入性方法比线程安全方法更受约束。只有当每次调用只访问唯一数据(如局部变量)时,才能从多个线程同时安全调用重入性方法。要使重入性方法成为线程安全的,需要对实例变量和共享数据的访问使用锁。
以下是一个简单的表格来对比线程安全和重入性:
| 特性 | 定义 | 保障方式 |
| ---- | ---- | ---- |
| 线程安全 | 多线程可同时调用对象方法,系统自动处理共享数据访问 | 依靠底层系统机制 |
| 重入性 | 每次调用只访问唯一数据时,多线程可同时调用 | 需要对共享数据访问加锁 |
10. 代码优化建议
在多线程编程中,为了提高性能和代码的可维护性,可以考虑以下优化建议:
-
减少锁的持有时间
:在使用锁保护共享数据时,尽量减少在锁的作用域内进行的处理操作。例如,在Page Indexer应用程序中,将发送响应的操作放在锁的作用域之外,以减少锁的持有时间,提高并发性能。
-
使用上下文管理器
:如前面提到的,Python 2.6及以上版本可以使用
with
关键字结合上下文管理器来替代
try...finally
块,使代码更简洁,同时确保锁的正确解锁。
-
合理使用读写锁
:当读操作频繁而写操作较少时,使用
QReadWriteLock
可以提高并发性能。因为多个线程可以同时持有读锁,而写锁是独占的。
11. 常见问题及解决方法
在多线程编程中,可能会遇到一些常见问题,下面是一些问题及对应的解决方法:
| 问题 | 现象 | 解决方法 |
| ---- | ---- | ---- |
| 死锁 | 线程相互等待对方释放锁,导致程序卡死 | 确保锁的获取顺序一致,减少锁的嵌套使用 |
| 数据竞争 | 多个线程同时访问和修改共享数据,导致数据不一致 | 使用同步机制,如互斥锁、读写锁等保护共享数据 |
| 界面冻结 | 长时间的处理任务阻塞主线程,导致界面无响应 | 将耗时任务放到二级线程中执行,使用信号 - 槽机制更新界面 |
12. 总结与展望
通过本文的介绍,我们深入了解了如何使用PyQt进行多线程编程,包括创建线程服务器、管理二级线程、处理线程同步和用户交互等方面。多线程编程可以显著提高应用程序的性能和响应能力,特别是在处理网络连接和耗时任务时。
在未来的开发中,可以进一步探索以下方向:
-
更复杂的线程管理
:例如,实现线程池来管理多个二级线程,提高资源利用率。
-
分布式多线程编程
:结合网络编程,将多线程应用扩展到分布式系统中。
-
与其他技术的集成
:如与数据库、机器学习等技术结合,开发更强大的应用程序。
总之,PyQt的多线程编程为开发高效、响应灵敏的GUI应用程序提供了强大的支持,通过合理运用这些技术,可以创造出更加优秀的软件产品。
超级会员免费看
228

被折叠的 条评论
为什么被折叠?



