- 内容提要
- 作者简介
- 技术评审者简介
- 致谢
- 译者序 会编程的人不一样
- 前言
- 本书的读者对象
- 编码规范
- 什么是编程
- 本书简介
- 下载和安装 Python
- 启动 IDLE
- 如何寻求帮助
- 聪明地提出编程问题
- 小结
- 第一部分 Python 编程基础
- 第1章 Python 基础
- 第2章 控制流
- 第3章 函数
- 第4章 列表
- 第5章 字典和结构化数据
- 第6章 字符串操作
- 第二部分 自动化任务
- 第7章 模式匹配与正则表达式
- 第8章 读写文件
- 第9章 组织文件
- 第10章 调试
- 第11章 从 Web 抓取信息
- 第12章 处理 Excel 电子表格
- 第13章 处理 PDF 和 Word 文档
- 第14章 处理 CSV 文件和 JSON 数据
- 第15章 保持时间、计划任务和启动程序
- 第16章 发送电子邮件和短信
- 第17章 操作图像
- 第18章 用 GUI 自动化控制键盘和鼠标
- 附录A 安装第三方模块
- 附录B 运行程序
- 附录C 习题答案
15.6 多线程
为了引入多线程的概念,让我们来看一个例子。假设你想安排一些代码,在一段延迟后或在特定时间运行。可以在程序启动时添加如下代码:
import time, datetime startTime = datetime.datetime(2029, 10, 31, 0, 0, 0) while datetime.datetime.now() < startTime: time.sleep(1) print('Program now starting on Halloween 2029') --snip--
这段代码指定2029年10月31日作为开始时间,不断调用time.sleep(1),直到开始时间。在等待time.sleep()的循环调用完成时,程序不能做任何事情,它只是坐在那里,直到2029年万圣节。这是因为Python程序在默认情况下,只有一个执行线程。
要理解什么是执行线程,就要回忆第2章关于控制流的讨论,当时你想象程序的执行就像把手指放在一行代码上,然后移动到下一行,或是流控制语句让它去的任何地方。单线程程序只有一个“手指”。但多线程的程序有多个“手指”。每个“手指”仍然移动到控制流语句定义的下一行代码,但这些“手指”可以在程序的不同地方,同时执行不同的代码行(到目前为止,本书所有的程序一直是单线程的)。
不必让所有的代码等待,直到time.sleep()函数完成,你可以使用Python的threading模块,在单独的线程中执行延迟或安排的代码。这个单独的线程将因为time.sleep()调用而暂停。同时,程序可以在原来的线程中做其他工作。
要得到单独的线程,首先要调用threading.Thread()函数,生成一个Thread对象。在新的文件中输入以下代码,并保存为threadDemo.py:
import threading, time print('Start of program.') ❶ def takeANap(): time.sleep(5) print('Wake up!') ❷ threadObj = threading.Thread(target=takeANap) ❸ threadObj.start() print('End of program.')
在❶行,我们定义了一个函数,希望用于新线程中。为了创建一个Thread对象,我们调用threading.Thread(),并传入关键字参数target=takeANap❷。这意味着我们要在新线程中调用的函数是takeANap()。请注意,关键字参数是target=takeANap,而不是target=takeANap()。这是因为你想将takeANap()函数本身作为参数,而不是调用takeANap(),并传入它的返回值。
我们将threading.Thread()创建的Thread对象保存在threadObj中,然后调用threadObj.start()❸,创建新的线程,并开始在新线程中执行目标函数。如果运行该程序,输出将像这样:
Start of program. End of program. Wake up!
这可能有点令人困惑。如果print('End of program.')是程序的最后一行,你可能会认为,它应该是最后打印的内容。Wake up!在它后面是因为,当threadObj.start()被调用时,threadObj的目标函数运行在一个新的执行线程中。将它看成是第二根“手指”,出现在takeANap()函数开始处。主线程继续print('End of program.')。同时,新线程已执行了time.sleep(5)调用,暂停5秒钟。之后它从5秒钟小睡中醒来,打印了'Wake up!',然后从takeANap()函数返回。按时间顺序,'Wake up!'是程序最后打印的内容。
通常,程序在文件中最后一行代码执行后终止(或调用sys.exit())。但threadDemo.py有两个线程。第一个是最初的线程,从程序开始处开始,在print('End of program.')后结束。第二个线程是调用threadObj.start()时创建的,始于takeANap()函数的开始处,在takeANap()返回后结束。
在程序的所有线程终止之前,Python程序不会终止。在运行threadDemo.py时,即使最初的线程已经终止,第二个线程仍然执行time.sleep(5)调用。
15.6.1 向线程的目标函数传递参数
如果想在新线程中运行的目标函数有参数,可以将目标函数的参数传入threading.Thread()。例如,假设想在自己的线程中运行以下print()调用:
>>> print('Cats', 'Dogs', 'Frogs', sep=' & ') Cats & Dogs & Frogs
该print()调用有3个常规参数:'Cats'、 'Dogs'和'Frogs',以及一个关键字参数:sep= ' & '。常规参数可以作为一个列表,传递给threading.Thread()中的args关键字参数。关键字参数可以作为一个字典,传递给threading.Thread()中的kwargs关键字参数。
在交互式环境中输入以下代码:
>>> import threading >>> threadObj = threading.Thread(target=print, args=['Cats', 'Dogs', 'Frogs'], kwargs={'sep': ' & '}) >>> threadObj.start() Cats & Dogs & Frogs
为了确保参数'Cats'、'Dogs'和'Frogs'传递给新线程中的print(),我们将args=['Cats', 'Dogs', 'Frogs']传入threading.Thread()。为了确保关键字参数sep=' & '传递给新线程中的print(),我们将kwargs={'sep': '& '}传入threading.Thread()。
threadObj.start()调用将创建一个新线程来调用print()函数,它会传入'Cats'、'Dogs'和'Frogs'作为参数,以及' & '作为sep关键字参数。
下面创建新线程调用print()的方法是不正确的:
threadObj = threading.Thread(target=print('Cats', 'Dogs', 'Frogs', sep=' & '))
这行代码最终会调用print()函数,将它的返回值(print()的返回值总是无)作为target关键字参数。它没有传递print()函数本身。如果要向新线程中的函数传递参数,就使用threading.Thread()函数的args和kwargs关键字参数。
15.6.2 并发问题
可以轻松地创建多个新线程,让它们同时运行。但多线程也可能会导致所谓的并发问题。如果这些线程同时读写变量,导致互相干扰,就会发生并发问题。并发问题可能很难一致地重现,所以难以调试。
多线程编程本身就是一个广泛的主题,超出了本书的范围。必须记住的是:为了避免并发问题,绝不让多个线程读取或写入相同的变量。当创建一个新的Thread对象时,要确保其目标函数只使用该函数中的局部变量。这将避免程序中难以调试的并发问题。
注意
在http://nostarch.com/automatestuff/,有关于多线程编程的初学者教程。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论