>>> 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对象时,要确保其目标函数只使用该函数中的局部变量。
这将避免程序中难以调试的并发问题。