>>> from env_helper import info; info()
页面更新时间: 2024-01-13 11:51:14
运行环境:
Linux发行版本: Debian GNU/Linux 12 (bookworm)
操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
Python版本: 3.11.2
5.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')
这段代码指定2029年10月31日作为开始时间,不断调用 time.sleep(1)
,
直到开始时间。在等待 time.sleep()
的循环调用完成时,程序不能做任何事情,它只是坐在那里,
直到2029年万圣节。这是因为 Python
程序在默认情况下,只有一个执行线程。
要理解什么是执行线程,就要回忆关于控制流的讨论,当时你想象程序的执行就像把手指放在一行代码上, 然后移动到下一行,或是流控制语句让它去的任何地方。 单线程程序只有一个“手指”。但多线程的程序有多个“手指”。 每个“手指”仍然移动到控制流语句定义的下一行代码,但这些“手指”可以在程序的不同地方, 同时执行不同的代码行)。
不必让所有的代码等待,直到 time.sleep()
函数完成, 你可以使用 Python
的 threading
模块,在单独的线程中执行延迟或安排的代码。
这个单独的线程将因为 time.sleep()
调用而暂停。
同时,程序可以在原来的线程中做其他工作。
5.6.1. 在 Python 中使用多线程¶
要得到单独的线程,首先要调用 threading.Thread()
函数,生成一个
Thread
对象。 在下面的实例中,定义了一个函数 takeANap()
,
希望用于新线程中。 为了创建一个 Thread
对象,我们调用
threading.Thread()
, 并传入关键字参数 target = takeANap
。
这意味着我们要在新线程中调用的函数是 takeANap()
。 请注意,
关键字参数是 target = takeANap
,而不是 target = takeANap()
。
这是因为你想将 takeANap()
函数本身作为参数,而不是调用
takeANap()
,并传入它的返回值。
我们将 threading.Thread()
创建的 Thread
对象保存在 threadObj
中,然后调用 threadObj.start()
,
创建新的线程,并开始在新线程中执行目标函数。
运行该程序,查看其输出:
>>> 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.')
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!'
是程序最后打印的内容。但这个不是这么明显的。 实际上不同线程执行没有固定的前后,这个程序可以认为是几乎同时开始两个线程, 而主线程由于print()
函数调用太快,所以才会马上打印出结果。
进一步再说明一下。
通常,程序在文件中最后一行代码执行后终止(或调用 sys.exit()
)。 但
threadDemo.py
有两个线程:
第一个是最初的线程,从程序开始处开始,在
print('End of program.')
后结束。第二个线程是调用
threadObj.start()
时创建的,始于takeANap()
函数的开始处,在takeANap()
返回后结束。
在程序的所有线程终止之前, Python
程序不会终止。
在运行上面代码时,即使最初的线程已经终止,第二个线程仍然执行
time.sleep(5)
调用。
5.6.2. 向线程的目标函数传递参数¶
如果想在新线程中运行的目标函数有参数,可以将目标函数的参数传入
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
Wake up!
为了确保参数 'Cats'
、 'Dogs'
和 'Trogs'
给新线程中的
print()
, 我们将 args=['Cats','DogsVFrogs']
传入
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
关键字参数。
5.6.3. 并发问题¶
可以轻松地创建多个新线程,让它们同时运行。但多线程也可能会导致所谓的并发问题。 如果这些线程同时读写变量,导致互相干扰,就会发生并发问题。并发问题可能很难一致地重现,所以难以调试。
多线程编程本身就是一个广泛的主题,超出了本书的范围。
必须记住的是:为了避免并发问题,绝不让多个线程读取或写入相同的变量。
当创建一个新的
Thread
对象时,要确保其目标函数只使用该函数中的局部变量。
这将避免程序中难以调试的并发问题。