返回介绍

15.6 多线程

发布于 2024-01-22 21:44:06 字数 3500 浏览 0 评论 0 收藏 0

为了引入多线程的概念,让我们来看一个例子。假设你想安排一些代码,在一段延迟后或在特定时间运行。可以在程序启动时添加如下代码:

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文