1. 前言

刚开始接触多线程编程的时候,对于守护线程和 Thread 对象的 join() 方法理解的不是很清楚,经过一段时间的学习和思考,现在大致搞明白了,所以在这里记录一下,如果错误,请不吝指正。

2. 守护线程

Python 官方文档中是这样描述守护线程的:

一个线程可以被标记成一个 “守护线程” 。这个标志的意义是,当没有存活的非守护线程时,整个Python程序才会退出。

这里描述的比较简短,可能无法很好地理解。

在《Python 核心编程》中也有一段对守护线程的说明:

守护线程一般是一个等待客户端请 求服务的服务器。 如果没有客户端请求, 守护线程就是空闲的。 如果把一个线程设置 为守护线程,就表示这个线程是不重要的,进程退出时不需要等待这个线程执行完成。

如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置守护 线程标记。该标记值为真时,表示该线程是不重要的,或者说该线程只是用来等待客户端请求而不做任何其他事情。

一个新的子线程会继承父线程的守护标记。整个 Python 程序(可以解读为:主线程)将在所有非守护线程退出之后才退出, 换句话说, 就是没有剩下存活的非守护线程时。

2.1 守护线程示例

上面的这段说明应该解释的比较清楚了,下面我们用代码来验证下。

import logging
import time
from threading import Thread, current_thread

logging.basicConfig(
    level=logging.INFO,
    format='[%(threadName)-10s] %(message)s'
)


def wait(n):
    s = time.time()
    time.sleep(n)
    e = time.time()
    logging.info(f'{current_thread()} has slept for {e - s:.2f} seconds and exit now')


def main(flag=False):
    sub1 = Thread(target=wait, args=(1,), name='sub1')
    sub2 = Thread(target=wait, args=(2,), name='sub2', daemon=flag)
    sub1.start()
    sub2.start()


if __name__ == '__main__':
    main()

上面代码的输出为:

[sub1      ] <Thread(sub1, started 123145363836928)> has slept for 1.00 seconds and exit now
[sub2      ] <Thread(sub2, started 123145369092096)> has slept for 2.00 seconds and exit now

可以看到,两个子线程都输出了内容。说明sub1sub2 两个子线程是非守护线程时,主线程是在这两个子线程都退出后才退出的。

作为对比,下面我们将其中一个子线程设置为守护线程:

import logging
import time
from threading import Thread, current_thread

logging.basicConfig(
    level=logging.INFO,
    format='[%(threadName)-10s] %(message)s'
)

def wait(n):
    s = time.time()
    time.sleep(n)
    e = time.time()
    logging.info(f'{current_thread()} has slept for {e - s:.2f} seconds and exit now')


def main(flag=False):
    sub1 = Thread(target=wait, args=(1,), name='sub1')
    sub2 = Thread(target=wait, args=(2,), name='sub2', daemon=flag)
    sub1.start()
    sub2.start()
   

if __name__ == '__main__':
    main(True) # 将 sub2 设置为守护线程

上面代码的输出为:

[sub1      ] <Thread(sub1, started 123145437872128)> has slept for 1.00 seconds and exit now

可以看到,只有子线程 sub1 打印了输出,说明主线程在 sub1 退出之后、sub2 退出之前退出了。

Python 官方文档中还有一段对于守护线程的重要说明:

守护线程在程序关闭时会突然关闭。他们的资源(例如已经打开的文档,数据库事务等等)可能没有被正确释放。如果你想你的线程正常停止,请将其设置为非守护模式并且使用合适的信号机制。

因此,将线程设置为守护线程时一定要确保在其退出前释放所有使用的资源。

3. Thread 对象的 join 方法

Thread.join() 方法的签名为:join(timeout=None)

Python 官方文档中对于该方法的说明如下:

调用该方法会阻塞调用这个方法的线程,直到被调用 join() 方法的线程终结 —— 不管是正常终结还是抛出未处理异常 —— 或者直到发生超时,超时选项是可选的。

timeout 参数存在而且不是 None 时,它应该是一个用于指定操作超时的以秒为单位的浮点数(或者分数)。因为该方法总是返回 None ,所以你一定要在调用该方法后调用 is_alive() 方法才能判断 join()方法是否发生超时 —— 如果线程仍然存活,说明 join()方法超时。

timeout 参数不存在或者是 None 时 ,调用该方法会一直阻塞,直到线程终结。

一个线程可以被 join() 很多次。

如果尝试 join 当前线程会导致死锁,join()方法会引发 RuntimeError 异常。如果在一个尚未开始的线程上调用 join() 方法,也会抛出相同的异常。

关于 join() 方法,有几个比较重要的点需要说明和厘清,为了便与描述,假设有两个线程 Caller 和 Sub:

  1. 在线程 Caller 中调用线程 Sub 的join() 方法会阻塞 Caller,而不是阻塞 Sub。
  2. 在线程 Caller 中调用线程 Sub 的 join() 方法时若未指定 timeout 参数或指定了 timeout 参数但其值为 None,则 Caller 将在 Sub 终结之后才继续执行。
  3. 在线程 Caller 中调用线程 Sub 的 join() 方法时若指定了非 Nonetimeout 参数,则 Caller 将在 Sub 终结之后或者经过了 timeout 秒之后继续执行。
  4. 调用 Sub 的 join() 方法超时之后 Sub 仍然是存活的,Sub 会继续运行。
  5. 若 Sub 尚未调用调用 start() 方法,那么调用 Sub 的 join() 方法会引发 RuntimeError 异常。
  6. 如果表达式 threading.current_thread() is CallerTrue,即 Caller 为当前线程,则会引发 RuntimeError 异常。
  7. 如果 Caller 尚未初始化,即 Thread.__init__() 方法尚未调用,则会引发 RuntimeError
  8. join()方法可以被调用多次。
  9. Sub 终结之后再调用其 join() 方法无任何作用。

3.1 示例

下面我们通过几个例子来练习一下。

3.1.1 示例 2

import logging
import time
from threading import Thread, current_thread

logging.basicConfig(
    level=logging.INFO,
    format='[%(threadName)-10s] %(message)s'
)

def wait(n):
    s = time.time()
    time.sleep(n)
    e = time.time()
    logging.info(f'{current_thread()} has slept for {e - s:.2f} seconds and exit now')


def main():
    sub1 = Thread(target=wait, args=(1,), name='sub1')
    sub1.start()


if __name__ == '__main__':
    start = time.time()
    main()
    logging.info('-' * 80)
    end = time.time()
    logging.info(f'elapsed time is {end - start:.4f} second(s)')

上面代码的输出为;

[MainThread] --------------------------------------------------------------------------------
[MainThread] elapsed time is 0.0002 second(s)
[sub1      ] <Thread(sub1, started 123145523101696)> has slept for 1.00 seconds and exit now

从打印的输出中可以看到,子线程 sub1 在主线程打印了运行时间之后才退出。这是由于子线程 sub1 的执行时间超过了 1 秒,主线程在 main() 函数执行结束前就已经打印出了执行时间,这个执行时间只是主线程的执行时间,并未包括子线程 sub1 的执行时间。

我们修改代码为如下形式;

import logging
import time
from threading import Thread, current_thread

logging.basicConfig(
    level=logging.INFO,
    format='[%(threadName)-10s] %(message)s'
)

def wait(n):
    s = time.time()
    time.sleep(n)
    e = time.time()
    logging.info(f'{current_thread()} has slept for {e - s:.2f} seconds and exit now')


def main():
    sub1 = Thread(target=wait, args=(1,), name='sub1')
    sub1.start()
    sub1.join() # 添加了这行代码


if __name__ == '__main__':
    start = time.time()
    main()
    logging.info('-' * 80)
    end = time.time()
    logging.info(f'elapsed time is {end - start:.4f} second(s)')

上面代码的输出为:

[sub1      ] <Thread(sub1, started 123145461538816)> has slept for 1.00 seconds and exit now
[MainThread] --------------------------------------------------------------------------------
[MainThread] elapsed time is 1.0048 second(s)

这次,在主线程打印运行时间之前子线程 sub1 已经退出,所以打印的运行时间包含了子线程 sub1 的运行时间。

3.1.2 示例 2

上面的例子演示了不带参数的 join() 方法,下面我们来演示下带 timeout 参数的 join() 方法。

from threading import Thread, current_thread, enumerate
import time
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='[%(threadName)-10s] %(message)s'
)


def wait(n):
    s = time.time()
    time.sleep(n)
    e = time.time()
    logging.info(f'{current_thread()} has slept for {e - s:.2f} seconds and exit now')


def main():
    sub1 = Thread(target=wait, args=(2,), name='sub1')
    sub1.start()
    sub1.join(1)
    logging.info(f'these thread still alive: {enumerate()}')


if __name__ == '__main__':
    start = time.time()
    main()
    logging.info('-' * 80)
    end = time.time()
    logging.info(f'elapsed time is {end - start:.4f} second(s)')

上面代码的输出为:

[MainThread] these thread still alive: [<_MainThread(MainThread, started 4451128768)>, <Thread(sub1, started 123145493172224)>]
[MainThread] --------------------------------------------------------------------------------
[MainThread] elapsed time is 1.0046 second(s)
[sub1      ] <Thread(sub1, started 123145493172224)> has slept for 2.00 seconds and exit now

可以看到,sub1 的 join 操作的超时时间设置为了 1 秒,而 sub1 在执行过程中会睡眠 2 秒,所以在 sub1 执行完成之前,sub1 的 join 操作会超时,而超时之后sub1 仍然是存活的。