最近在完善 teelebot 的多线程机制时,用到了 Python模块 concurrent.futuresThreadPoolExecutor 类。通过 ThreadPoolExecutor构建线程池非常简单:

from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(5) as thread_pool:
    thread_pool.submit(function, argument)
    ...

但是,这样去使用,并不会捕获子线程中的异常。也就是说,若 function 在执行过程中抛出了错误,并不会被主线程捕获后输出到命令行,而是看上去一切正常。后来查看文档,若想捕获子线程的异常,应当使用 exception(timeout=None) :

...
fur = thread_pool.submit(function, argument)
print(fur.exception())

但直接使用 exception ,会阻塞主线程,并等待调用执行完毕。若想以非阻塞的形式捕获,则需要结合使用 add_done_callback(fn)add_done_callback(fn) 的作用是,当线程被取消或者结束运行时,调用add_done_callback 的唯一参数 fnfn 是一个可调用的对象,可以是一个函数。结合 add_done_callback 的代码如下:

from concurrent.futures import ThreadPoolExecutor

def exception_callback(fur):
    if fur.exception() != None: #若线程正常退出,返回None
        print(fur.exception())
     

with ThreadPoolExecutor(5) as thread_pool:
    fur = thread_pool.submit(function, argument)
    fur.add_done_callback(exception_callback)

这样就能成功捕获并输出子线程的异常。但这样输出仍然有一个问题,就是报错信息过于简单,以 NameError 为例,只有简短的一句话:

NameError: name 'i' is not defined

并不会给出具体的文件,出错的代码和行数,是非常不利于调试的。若想输出详细的报错,则需要使用到另外一个方法:result(timeout=None)result 的作用是返回调用返回的值,它也会阻塞主线程。 结合 result最终示例代码如下:

# -*- coding:utf-8 -*-
from concurrent.futures import ThreadPoolExecutor

def do_something(m):
    print(m)
    print(i) #打印并不存在的i,模拟异常

def exception_callback(fur):
    if fur.exception() != None: #若线程正常退出,返回None
        print(fur.result()) #判断是否存在异常,存在则打印返回的值

if __name__ == "__main__":
    with ThreadPoolExecutor(5) as thread_pool:
        fur = thread_pool.submit(do_something, "hello python!")
        fur.add_done_callback(exception_callback)

输出:

hello python!
exception calling callback for <Future at 0x23c90dd88e0 state=finished raised NameError>
Traceback (most recent call last):
  File "C:\Users\UserName\AppData\Local\Programs\Python\Python38\lib\concurrent\futures\_base.py", line 328, in _invoke_callbacks
    callback(self)
  File "C:\Users\UserName\Desktop\futures.py", line 10, in exception_callback
    print(fur.result())
  File "C:\Users\UserName\AppData\Local\Programs\Python\Python38\lib\concurrent\futures\_base.py", line 432, in result
    return self.__get_result()
  File "C:\Users\UserName\AppData\Local\Programs\Python\Python38\lib\concurrent\futures\_base.py", line 388, in __get_result
    raise self._exception
  File "C:\Users\UserName\AppData\Local\Programs\Python\Python38\lib\concurrent\futures\thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "C:\Users\UserName\Desktop\futures.py", line 6, in do_something
    print(i) #打印并不存在的i,模拟异常
NameError: name 'i' is not defined

参考链接