为了引入多线程的概念,来看一个例子。假设想安排一些代码, 在一段延迟后或在特定时间运行。可以在程序启动时添加如下代码:
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 程序在默认情况下,只有一个执行线程。
要理解什么是执行线程,就要回忆关于控制流的讨论,当时想象程序的执行就像把手指放在一行代码上, 然后移动到下一行,或是流控制语句让它去的任何地方。 单线程程序只有一个“手指”。但多线程的程序有多个“手指”。 每个“手指”仍然移动到控制流语句定义的下一行代码,但这些“手指”可以在程序的不同地方, 同时执行不同的代码行)。
不必让所有的代码等待,直到 time.sleep() 函数完成, 可以使用 Python 的 threading 模块,在单独的线程中执行延迟或安排的代码。 这个单独的线程将因为 time.sleep() 调用而暂停。 同时,程序可以在原来的线程中做其他工作。
在 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()
Start of program. Wake up!
print('End of program.')
End of program.
这可能有点令人困惑。因为 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) 调用。
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' 和 '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关键字参数。
并发问题
可以轻松地创建多个新线程,让它们同时运行。但多线程也可能会导致所谓的并发问题。 如果这些线程同时读写变量,导致互相干扰,就会发生并发问题。并发问题可能很难一致地重现,所以难以调试。
多线程编程本身就是一个广泛的主题。 必须记住的是:为了避免并发问题,绝不让多个线程读取或写入相同的变量。 当创建一个新的 Thread对象时,要确保其目标函数只使用该函数中的局部变量。 这将避免程序中难以调试的并发问题。