41、PyQt多线程编程:创建线程服务器与管理二级线程

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应用程序提供了强大的支持,通过合理运用这些技术,可以创造出更加优秀的软件产品。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值