42、多线程编程:从单线程到多线程的页面索引器优化

多线程编程:从单线程到多线程的页面索引器优化

1. 引言

在软件开发中,多线程编程是提高程序性能和响应能力的重要手段。本文将详细介绍如何实现一个页面索引器的二级线程,并探讨如何将其从单线程优化为多线程版本。

2. 二级线程的实现

在页面索引器中,二级线程通过 Walker 类实现,该类位于 chap19/walker.py 文件中。 Walker 类继承自 QThread ,使用 QMutex 保护对自身私有数据的访问,并使用 QReadWriteLock 保护与主线程共享的数据。

class Walker(QThread):
    COMMON_WORDS_THRESHOLD = 250
    MIN_WORD_LEN = 3
    MAX_WORD_LEN = 25
    INVALID_FIRST_OR_LAST = frozenset("0123456789_")
    STRIPHTML_RE = re.compile(r"<[^>]*?>", re.IGNORECASE|re.MULTILINE)
    ENTITY_RE = re.compile(r"&(\w+?);|&#(\d+?);")
    SPLIT_RE = re.compile(r"\W+", re.IGNORECASE|re.MULTILINE)

    def __init__(self, lock, parent=None):
        super(Walker, self).__init__(parent)
        self.lock = lock
        self.stopped = False
        self.mutex = QMutex()
        self.path = None
        self.completed = False

    def initialize(self, path, filenamesForWords, commonWords):
        self.stopped = False
        self.path = path
        self.filenamesForWords = filenamesForWords
        self.commonWords = commonWords
        self.completed = False

    def run(self):
        self.processFiles(self.path)
        self.stop()
        self.emit(SIGNAL("finished(bool)"), self.completed)

    def stop(self):
        try:
            self.mutex.lock()
            self.stopped = True
        finally:
            self.mutex.unlock()

    def isStopped(self):
        try:
            self.mutex.lock()
            return self.stopped
        finally:
            self.mutex.unlock()

    def processFiles(self, path):
        def unichrFromEntity(match):
            text = match.group(match.lastindex)
            if text.isdigit():
                return unichr(int(text))
            u = htmlentitydefs.name2codepoint.get(text)
            return unichr(u) if u is not None else ""

        for root, dirs, files in os.walk(path):
            if self.isStopped():
                return
            for name in [name for name in files if name.endswith((".htm", ".html"))]:
                fname = os.path.join(root, name)
                if self.isStopped():
                    return
                words = set()
                fh = None
                try:
                    fh = codecs.open(fname, "r", "UTF8", "ignore")
                    text = fh.read()
                except (IOError, OSError), e:
                    sys.stderr.write("Error: %s\n" % e)
                    continue
                finally:
                    if fh is not None:
                        fh.close()
                if self.isStopped():
                    return
                text = self.STRIPHTML_RE.sub("", text)
                text = self.ENTITY_RE.sub(unichrFromEntity, text)
                text = text.lower()
                for word in self.SPLIT_RE.split(text):
                    if self.MIN_WORD_LEN <= len(word) <= self.MAX_WORD_LEN and \
                            word[0] not in self.INVALID_FIRST_OR_LAST and \
                            word[-1] not in self.INVALID_FIRST_OR_LAST:
                        try:
                            self.lock.lockForRead()
                            new = word not in self.commonWords
                        finally:
                            self.lock.unlock()
                        if new:
                            words.add(word)
                    if self.isStopped():
                        return
                for word in words:
                    try:
                        self.lock.lockForWrite()
                        files = self.filenamesForWords[word]
                        if len(files) > self.COMMON_WORDS_THRESHOLD:
                            del self.filenamesForWords[word]
                            self.commonWords.add(word)
                        else:
                            files.add(unicode(fname))
                    finally:
                        self.lock.unlock()
                self.emit(SIGNAL("indexed(QString)"), fname)
                self.completed = True
2.1 类的静态变量
  • COMMON_WORDS_THRESHOLD :定义一个单词在多少个文件中出现后被视为常用词。
  • MIN_WORD_LEN MAX_WORD_LEN :定义单词的最小和最大长度。
  • INVALID_FIRST_OR_LAST :定义单词不能以哪些字符开头或结尾。
  • STRIPHTML_RE :用于去除 HTML 标签的正则表达式。
  • ENTITY_RE :用于提取实体并将其转换为 Unicode 字符的正则表达式。
  • SPLIT_RE :用于将文件文本拆分为单词的正则表达式。
2.2 初始化方法
  • __init__ :初始化线程对象,接收一个锁和可选的父对象作为参数。
  • initialize :在调用 QThread.start() 之前调用,设置线程的路径、文件名字典和常用词集合。
2.3 线程运行方法
  • run :线程启动时调用的方法,调用 processFiles 方法处理文件,并在处理完成后停止线程并发出 finished 信号。
  • processFiles :递归遍历指定路径下的所有 HTML 文件,提取单词并更新文件名字典和常用词集合。
2.4 线程控制方法
  • stop :停止线程,使用 QMutex 保护对 stopped 变量的访问。
  • isStopped :检查线程是否已停止,同样使用 QMutex 保护对 stopped 变量的访问。
3. 线程同步与数据保护

在多线程编程中,线程同步和数据保护是至关重要的。 Walker 类使用 QMutex QReadWriteLock 来确保数据的一致性和线程安全。
- QMutex :用于保护对 stopped 变量的访问,防止多个线程同时修改该变量。
- QReadWriteLock :用于保护对共享数据( filenamesForWords 字典和 commonWords 集合)的访问,允许多个线程同时读取数据,但在写入数据时进行独占访问。

4. 线程执行流程

下面是 Walker 线程的执行流程:

graph TD;
    A[初始化线程] --> B[调用initialize方法设置参数];
    B --> C[调用start方法启动线程];
    C --> D[调用run方法];
    D --> E[调用processFiles方法处理文件];
    E --> F{是否停止线程};
    F -- 是 --> G[停止线程并发出finished信号];
    F -- 否 --> H[继续处理文件];
    H --> F;
5. 多线程优化思路

为了提高页面索引器的性能,可以将其从单线程优化为多线程版本。具体思路如下:
- 将 os.walk() 方法移到主线程,创建一个文件名列表。
- 每当列表中有 1000 个文件时,创建一个二级线程来处理这些文件。
- 最后,创建一个二级线程来处理剩余的文件。

6. 优化步骤
  1. 修改 setPath 方法 :在 setPath 方法中收集文件名并创建二级线程来处理它们。
  2. 创建辅助方法 :使用一个单独的方法来创建二级线程。
  3. 添加线程管理方法 :添加 stopWalkers 方法来停止所有二级线程,并修改 finished accept reject finishedIndexing 方法。
  4. 保护用户界面 :使用 QMutex 保护对用户界面中可能被多个线程访问的小部件的访问。
  5. 删除不再需要的线程 :确保在不再需要线程时将其删除,以避免每次调用 setPath 方法时创建越来越多的线程。
7. 总结

通过使用多线程编程,可以显著提高页面索引器的性能和响应能力。在实现多线程程序时,需要注意线程同步和数据保护,以确保程序的正确性和稳定性。同时,还可以通过合理分配任务和优化线程管理来进一步提高程序的性能。

8. 拓展资源

Python 和 PyQt 提供了丰富的库和工具,可以帮助我们更高效地进行多线程编程。以下是一些有用的资源:
- Python 标准库 :包含了许多用于多线程编程的模块,如 threading queue
- PyQt 文档 :提供了详细的类和方法说明,以及丰富的示例代码。
- Python 包索引(PyPI) :可以找到许多第三方库和工具,用于扩展 Python 和 PyQt 的功能。
- Python 食谱 :提供了许多实用的技巧和示例,帮助我们解决实际编程中的问题。

通过不断学习和实践,我们可以更好地掌握多线程编程的技巧,开发出更加高效、稳定的应用程序。

多线程编程:从单线程到多线程的页面索引器优化

9. 多线程优化的代码实现

在前面我们提到了多线程优化的思路和步骤,下面我们来看具体的代码实现。

首先,我们需要修改 setPath 方法,将 os.walk() 移到主线程,并创建文件名列表,当列表达到 1000 个文件时创建二级线程处理。

# 在主程序中修改 setPath 方法
def setPath(self, path):
    filenames = []
    for root, dirs, files in os.walk(path):
        for name in [name for name in files if name.endswith((".htm", ".html"))]:
            fname = os.path.join(root, name)
            filenames.append(fname)
            if len(filenames) == 1000:
                self.create_walker_thread(filenames)
                filenames = []
    if filenames:
        self.create_walker_thread(filenames)

def create_walker_thread(self, filenames):
    walker = Walker(self.lock)
    # 这里可以直接将参数传递给构造函数,不需要 initialize 方法
    walker.filenames = filenames
    walker.filenamesForWords = self.filenamesForWords
    walker.commonWords = self.commonWords
    walker.start()

同时,我们需要添加 stopWalkers 方法来停止所有二级线程:

def stopWalkers(self):
    for walker in self.walkers:
        walker.stop()

并且修改 finished accept reject finishedIndexing 方法,确保线程管理的正确性。

10. 线程安全与性能考量

在多线程编程中,线程安全和性能是两个重要的考量因素。

考量因素 说明
线程安全 使用 QMutex QReadWriteLock 等机制保护共享数据,避免数据竞争和不一致问题。例如,在 Walker 类中,使用 QMutex 保护 stopped 变量,使用 QReadWriteLock 保护 filenamesForWords 字典和 commonWords 集合。
性能 在保护机制内部尽量减少工作量,以减少其他线程的阻塞时间。对于读取数据,如果数据量不大,最好进行复制,避免在保护范围外访问数据。同时,合理安排线程的任务分配,避免线程之间的过度竞争。
11. 多线程页面索引器的执行流程

下面是多线程页面索引器的执行流程:

graph TD;
    A[设置路径] --> B[主线程遍历目录收集文件名];
    B --> C{文件名列表是否达到 1000 个};
    C -- 是 --> D[创建二级线程处理 1000 个文件];
    C -- 否 --> E{是否遍历完所有文件};
    E -- 否 --> B;
    E -- 是 --> F[创建二级线程处理剩余文件];
    D --> G[二级线程处理文件并更新共享数据];
    F --> G;
    G --> H{是否停止线程};
    H -- 是 --> I[停止线程并发出 finished 信号];
    H -- 否 --> G;
12. 与单线程版本的对比

多线程版本的页面索引器相比单线程版本具有明显的优势:
- 性能提升 :通过合理分配任务到多个线程,可以并行处理多个文件,从而显著提高索引速度。
- 响应能力增强 :主线程可以在二级线程处理文件的同时响应用户操作,提高用户体验。

然而,多线程编程也带来了一些挑战,如线程同步和数据保护的复杂性,需要开发者更加小心地处理。

13. 总结与展望

通过将页面索引器从单线程优化为多线程版本,我们可以看到多线程编程在提高程序性能和响应能力方面的巨大潜力。在实现过程中,我们需要注意线程同步和数据保护,确保程序的正确性和稳定性。

未来,我们可以进一步探索多线程编程的优化策略,例如使用线程池来管理线程,减少线程创建和销毁的开销;或者根据文件的大小和复杂度动态分配线程任务,以实现更高效的资源利用。同时,我们还可以结合其他技术,如异步编程,进一步提升程序的性能。

总之,多线程编程是一个强大的工具,通过不断学习和实践,我们可以更好地掌握它,开发出更加高效、稳定的应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值