为了引入多线程的概念,来看一个例子。假设想安排一些代码, 在一段延迟后或在特定时间运行。可以在程序启动时添加如下代码:
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
对象时,要确保其目标函数只使用该函数中的局部变量。
这将避免程序中难以调试的并发问题。