多线程编程:从单线程到多线程的页面索引器优化
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. 优化步骤
-
修改
setPath方法 :在setPath方法中收集文件名并创建二级线程来处理它们。 - 创建辅助方法 :使用一个单独的方法来创建二级线程。
-
添加线程管理方法
:添加
stopWalkers方法来停止所有二级线程,并修改finished、accept、reject和finishedIndexing方法。 -
保护用户界面
:使用
QMutex保护对用户界面中可能被多个线程访问的小部件的访问。 -
删除不再需要的线程
:确保在不再需要线程时将其删除,以避免每次调用
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. 总结与展望
通过将页面索引器从单线程优化为多线程版本,我们可以看到多线程编程在提高程序性能和响应能力方面的巨大潜力。在实现过程中,我们需要注意线程同步和数据保护,确保程序的正确性和稳定性。
未来,我们可以进一步探索多线程编程的优化策略,例如使用线程池来管理线程,减少线程创建和销毁的开销;或者根据文件的大小和复杂度动态分配线程任务,以实现更高效的资源利用。同时,我们还可以结合其他技术,如异步编程,进一步提升程序的性能。
总之,多线程编程是一个强大的工具,通过不断学习和实践,我们可以更好地掌握它,开发出更加高效、稳定的应用程序。
超级会员免费看

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



