在调用者线程中捕获线程的异常?
- 2024-12-30 08:42:00
- admin 原创
- 118
问题描述:
我对 Python 和多线程编程还很陌生。基本上,我有一个脚本会将文件复制到另一个位置。我希望将其放在另一个线程中,这样我就可以输出....
以表明该脚本仍在运行。
我遇到的问题是,如果无法复制文件,则会引发异常。如果在主线程中运行,这是可以的;但是,以下代码不起作用:
try:
threadClass = TheThread(param1, param2, etc.)
threadClass.start() ##### **Exception takes place here**
except:
print "Caught an exception"
在线程类本身中,我尝试重新抛出异常,但不起作用。我看到这里的人们问过类似的问题,但他们似乎都在做一些比我想做的事情更具体的事情(而且我不太明白提供的解决方案)。我看到有人提到了 的用法sys.exc_info()
,但我不知道在哪里或如何使用它。
编辑:线程类的代码如下:
class TheThread(threading.Thread):
def __init__(self, sourceFolder, destFolder):
threading.Thread.__init__(self)
self.sourceFolder = sourceFolder
self.destFolder = destFolder
def run(self):
try:
shul.copytree(self.sourceFolder, self.destFolder)
except:
raise
解决方案 1:
问题是它thread_obj.start()
会立即返回。您生成的子线程在其自己的上下文中执行,具有自己的堆栈。发生的任何异常都在子线程的上下文中,并且位于其自己的堆栈中。我现在能想到的一种将此信息传达给父线程的方法是使用某种消息传递,因此您可以研究一下。
试穿一下这个尺码:
import sys
import threading
import queue
class ExcThread(threading.Thread):
def __init__(self, bucket):
threading.Thread.__init__(self)
self.bucket = bucket
def run(self):
try:
raise Exception('An error occured here.')
except Exception:
self.bucket.put(sys.exc_info())
def main():
bucket = queue.Queue()
thread_obj = ExcThread(bucket)
thread_obj.start()
while True:
try:
exc = bucket.get(block=False)
except queue.Empty:
pass
else:
exc_type, exc_obj, exc_trace = exc
# deal with the exception
print exc_type, exc_obj
print exc_trace
thread_obj.join(0.1)
if thread_obj.isAlive():
continue
else:
break
if __name__ == '__main__':
main()
解决方案 2:
这个问题有很多非常复杂的答案。我是不是把这个问题过于简单化了,因为在我看来,对于大多数事情来说,这似乎已经足够了。
from threading import Thread
class PropagatingThread(Thread):
def run(self):
self.exc = None
try:
if hasattr(self, '_Thread__target'):
# Thread uses name mangling prior to Python 3.
self.ret = self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
else:
self.ret = self._target(*self._args, **self._kwargs)
except BaseException as e:
self.exc = e
def join(self, timeout=None):
super(PropagatingThread, self).join(timeout)
if self.exc:
raise self.exc
return self.ret
如果您确定只会在 Python 的一个或另一个版本上运行,则可以将方法简化run()
为仅仅是混乱的版本(如果您只会在 3 之前的 Python 版本上运行),或者仅仅是干净的版本(如果您只会在从 3 开始的 Python 版本上运行)。
使用示例:
def f(*args, **kwargs):
print(args)
print(kwargs)
raise Exception('I suck at this')
t = PropagatingThread(target=f, args=(5,), kwargs={'hello':'world'})
t.start()
t.join()
当您加入时,您会看到另一个线程上引发的异常。
如果您仅在 Python 3 上使用six
或,则可以改进重新引发异常时获得的堆栈跟踪信息。您可以将内部异常包装在新的外部异常中,而不是仅在连接点处获取堆栈跟踪,并使用
six.raise_from(RuntimeError('Exception in thread'),self.exc)
或者
raise RuntimeError('Exception in thread') from self.exc
解决方案 3:
该concurrent.futures
模块可以轻松地在单独的线程(或进程)中完成工作并处理任何由此产生的异常:
import concurrent.futures
import shutil
def copytree_with_dots(src_path, dst_path):
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# Execute the copy on a separate thread,
# creating a future object to track progress.
future = executor.submit(shutil.copytree, src_path, dst_path)
while future.running():
# Print pretty dots here.
pass
# Return the value returned by shutil.copytree(), None.
# Raise any exceptions raised during the copy process.
return future.result()
concurrent.futures
包含在 Python 3.2 中,并可作为早期版本的反向移植futures
模块使用。
解决方案 4:
尽管无法直接捕获不同线程中抛出的异常,但这里有一段代码可以非常透明地获得与此功能非常接近的功能。您的子线程必须继承该类ExThread
而不是父线程,并且父线程必须在等待线程完成其工作时threading.Thread
调用该child_thread.join_with_exception()
方法而不是。child_thread.join()
此实现的技术细节:当子线程抛出异常时,该异常通过传递给父线程Queue
,并在父线程中再次抛出。请注意,此方法中没有忙等待。
#!/usr/bin/env python
import sys
import threading
import Queue
class ExThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.__status_queue = Queue.Queue()
def run_with_exception(self):
"""This method should be overriden."""
raise NotImplementedError
def run(self):
"""This method should NOT be overriden."""
try:
self.run_with_exception()
except BaseException:
self.__status_queue.put(sys.exc_info())
self.__status_queue.put(None)
def wait_for_exc_info(self):
return self.__status_queue.get()
def join_with_exception(self):
ex_info = self.wait_for_exc_info()
if ex_info is None:
return
else:
raise ex_info[1]
class MyException(Exception):
pass
class MyThread(ExThread):
def __init__(self):
ExThread.__init__(self)
def run_with_exception(self):
thread_name = threading.current_thread().name
raise MyException("An error in thread '{}'.".format(thread_name))
def main():
t = MyThread()
t.start()
try:
t.join_with_exception()
except MyException as ex:
thread_name = threading.current_thread().name
print "Caught a MyException in thread '{}': {}".format(thread_name, ex)
if __name__ == '__main__':
main()
解决方案 5:
如果线程中发生异常,最好的方法是在 期间在调用者线程中重新引发该join
异常。您可以使用 函数获取当前正在处理的异常的信息sys.exc_info()
。此信息可以简单地存储为线程对象的属性,直到join
被调用,此时可以重新引发该异常。
Queue.Queue
请注意,在这个简单的情况下,线程最多抛出 1 个异常并在抛出异常后立即完成,因此(如其他答案中所建议的) 不是必需的。我们只需等待线程完成即可避免竞争条件。
例如,扩展ExcThread
(如下)、覆盖excRun
(而不是run
)。
Python 2.x:
import threading
class ExcThread(threading.Thread):
def excRun(self):
pass
def run(self):
self.exc = None
try:
# Possibly throws an exception
self.excRun()
except:
import sys
self.exc = sys.exc_info()
# Save details of the exception thrown but don't rethrow,
# just complete the function
def join(self):
threading.Thread.join(self)
if self.exc:
msg = "Thread '%s' threw an exception: %s" % (self.getName(), self.exc[1])
new_exc = Exception(msg)
raise new_exc.__class__, new_exc, self.exc[2]
Python 3.x:
Python 3 中取消了的 3 个参数形式raise
,因此将最后一行更改为:
raise new_exc.with_traceback(self.exc[2])
解决方案 6:
我所做的是,简单地覆盖线程的连接和运行方法:
class RaisingThread(threading.Thread):
def run(self):
self._exc = None
try:
super().run()
except Exception as e:
self._exc = e
def join(self, timeout=None):
super().join(timeout=timeout)
if self._exc:
raise self._exc
使用方法如下:
def foo():
time.sleep(2)
print('hi, from foo!')
raise Exception('exception from foo')
t = RaisingThread(target=foo)
t.start()
try:
t.join()
except Exception as e:
print(e)
结果:
hi, from foo!
exception from foo!
解决方案 7:
concurrent.futures.as_completed
https://docs.python.org/3.7/library/concurrent.futures.html#concurrent.futures.as_completed
以下解决方案:
调用异常时立即返回主线程
不需要额外的用户定义类,因为它不需要:
明确
Queue
在工作线程周围添加 except else
来源:
#!/usr/bin/env python3
import concurrent.futures
import time
def func_that_raises(do_raise):
for i in range(3):
print(i)
time.sleep(0.1)
if do_raise:
raise Exception()
for i in range(3):
print(i)
time.sleep(0.1)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
futures = []
futures.append(executor.submit(func_that_raises, False))
futures.append(executor.submit(func_that_raises, True))
for future in concurrent.futures.as_completed(futures):
print(repr(future.exception()))
可能的输出:
0
0
1
1
2
2
0
Exception()
1
2
None
不幸的是,由于一个期货失败,无法通过终止期货来取消其他期货:
concurrent.futures
;Python:concurrent.futures 如何使其可取消?threading
:有什么办法可以终止线程吗?C pthreads:在 Pthread 库中终止线程
如果你做类似的事情:
for future in concurrent.futures.as_completed(futures):
if future.exception() is not None:
raise future.exception()
然后with
捕获它,并等待第二个线程完成后再继续。以下行为类似:
for future in concurrent.futures.as_completed(futures):
future.result()
如果发生异常,则会future.result()
重新引发异常。
如果您想退出整个 Python 进程,您可能可以通过os._exit(0)
,但这可能意味着您需要重构。
具有完美异常语义的自定义类
我最终在“限制同时运行的最大线程数的正确方法是什么? ”部分“带有错误处理的队列示例”中为自己编写了完美的界面。该类旨在既方便又让您完全控制提交和结果/错误处理。
在 Python 3.6.7、Ubuntu 18.04 上测试。
解决方案 8:
在 Python 3.8 中,我们可以使用threading.excepthook来钩住所有子线程中未捕获的异常!例如,
threading.excepthook = thread_exception_handler
参考者:https://stackoverflow.com/a/60002752/5093308
解决方案 9:
这是一个棘手的小问题,我想提出我的解决方案。我发现的其他一些解决方案(例如 async.io)看起来很有希望,但也呈现出一点黑盒。队列/事件循环方法有点将你与某个实现联系起来。然而,并发期货源代码只有大约 1000 行,而且很容易理解。它让我轻松解决了我的问题:创建临时工作线程而无需太多设置,并且能够在主线程中捕获异常。
我的解决方案使用了并发 Futures API 和线程 API。它允许您创建一个为您提供线程和 Future 的 Worker。这样,您就可以加入线程以等待结果:
worker = Worker(test)
thread = worker.start()
thread.join()
print(worker.future.result())
...或者你可以让工作人员在完成后发送回调:
worker = Worker(test)
thread = worker.start(lambda x: print('callback', x))
...或者您可以循环直至事件完成:
worker = Worker(test)
thread = worker.start()
while True:
print("waiting")
if worker.future.done():
exc = worker.future.exception()
print('exception?', exc)
result = worker.future.result()
print('result', result)
break
time.sleep(0.25)
代码如下:
from concurrent.futures import Future
import threading
import time
class Worker(object):
def __init__(self, fn, args=()):
self.future = Future()
self._fn = fn
self._args = args
def start(self, cb=None):
self._cb = cb
self.future.set_running_or_notify_cancel()
thread = threading.Thread(target=self.run, args=())
thread.daemon = True #this will continue thread execution after the main thread runs out of code - you can still ctrl + c or kill the process
thread.start()
return thread
def run(self):
try:
self.future.set_result(self._fn(*self._args))
except BaseException as e:
self.future.set_exception(e)
if(self._cb):
self._cb(self.future.result())
...和测试功能:
def test(*args):
print('args are', args)
time.sleep(2)
raise Exception('foo')
解决方案 10:
我知道我来晚了,但我遇到了非常类似的问题,但它包括使用 tkinter 作为 GUI,并且主循环使得无法使用任何依赖于 .join() 的解决方案。 因此,我调整了原始问题的编辑中给出的解决方案,但使其更通用,以便其他人更容易理解。
以下是新线程类的实际运行情况:
import threading
import traceback
import logging
class ExceptionThread(threading.Thread):
def __init__(self, *args, **kwargs):
threading.Thread.__init__(self, *args, **kwargs)
def run(self):
try:
if self._target:
self._target(*self._args, **self._kwargs)
except Exception:
logging.error(traceback.format_exc())
def test_function_1(input):
raise IndexError(input)
if __name__ == "__main__":
input = 'useful'
t1 = ExceptionThread(target=test_function_1, args=[input])
t1.start()
当然,您始终可以让它通过日志记录的其他方式处理异常,例如将其打印出来或将其输出到控制台。
这使您可以完全像使用 Thread 类一样使用 ExceptionThread 类,无需进行任何特殊修改。
解决方案 11:
我用的是这个版本,它很精简而且运行良好。
class SafeThread(threading.Thread):
def __init__(self, *args, **kwargs):
super(SafeThread, self).__init__(*args, **kwargs)
self.exception = None
def run(self) -> None:
try:
super(SafeThread, self).run()
except Exception as ex:
self.exception = ex
traceback.print_exc()
def join(self, *args, **kwargs) -> None:
super(SafeThread, self).join(*args, **kwargs)
if self.exception:
raise self.exception
要使用它,只需替换threading.Thread
为SafeThread
例如
t = SafeThread(target = some_function, args = (some, args,))
t.start()
# do something else here if you want as the thread runs in the background
t.join()
解决方案 12:
与 RickardSjogren 的方法类似,没有 Queue、sys 等,但也没有一些信号监听器:直接执行对应于 except 块的异常处理程序。
#!/usr/bin/env python3
import threading
class ExceptionThread(threading.Thread):
def __init__(self, callback=None, *args, **kwargs):
"""
Redirect exceptions of thread to an exception handler.
:param callback: function to handle occured exception
:type callback: function(thread, exception)
:param args: arguments for threading.Thread()
:type args: tuple
:param kwargs: keyword arguments for threading.Thread()
:type kwargs: dict
"""
self._callback = callback
super().__init__(*args, **kwargs)
def run(self):
try:
if self._target:
self._target(*self._args, **self._kwargs)
except BaseException as e:
if self._callback is None:
raise e
else:
self._callback(self, e)
finally:
# Avoid a refcycle if the thread is running a function with
# an argument that has a member that points to the thread.
del self._target, self._args, self._kwargs, self._callback
只有 self._callback 和 run() 中的 except 块是普通 threading.Thread 的附加块。
解决方案 13:
作为线程新手,我花了很长时间才理解如何实现 Mateusz Kobos 的代码(如上)。下面是简化版本,可帮助您理解如何使用它。
#!/usr/bin/env python
import sys
import threading
import Queue
class ExThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.__status_queue = Queue.Queue()
def run_with_exception(self):
"""This method should be overriden."""
raise NotImplementedError
def run(self):
"""This method should NOT be overriden."""
try:
self.run_with_exception()
except Exception:
self.__status_queue.put(sys.exc_info())
self.__status_queue.put(None)
def wait_for_exc_info(self):
return self.__status_queue.get()
def join_with_exception(self):
ex_info = self.wait_for_exc_info()
if ex_info is None:
return
else:
raise ex_info[1]
class MyException(Exception):
pass
class MyThread(ExThread):
def __init__(self):
ExThread.__init__(self)
# This overrides the "run_with_exception" from class "ExThread"
# Note, this is where the actual thread to be run lives. The thread
# to be run could also call a method or be passed in as an object
def run_with_exception(self):
# Code will function until the int
print "sleeping 5 seconds"
import time
for i in 1, 2, 3, 4, 5:
print i
time.sleep(1)
# Thread should break here
int("str")
# I'm honestly not sure why these appear here? So, I removed them.
# Perhaps Mateusz can clarify?
# thread_name = threading.current_thread().name
# raise MyException("An error in thread '{}'.".format(thread_name))
if __name__ == '__main__':
# The code lives in MyThread in this example. So creating the MyThread
# object set the code to be run (but does not start it yet)
t = MyThread()
# This actually starts the thread
t.start()
print
print ("Notice 't.start()' is considered to have completed, although"
" the countdown continues in its new thread. So you code "
"can tinue into new processing.")
# Now that the thread is running, the join allows for monitoring of it
try:
t.join_with_exception()
# should be able to be replace "Exception" with specific error (untested)
except Exception, e:
print
print "Exceptioon was caught and control passed back to the main thread"
print "Do some handling here...or raise a custom exception "
thread_name = threading.current_thread().name
e = ("Caught a MyException in thread: '" +
str(thread_name) +
"' [" + str(e) + "]")
raise Exception(e) # Or custom class of exception, such as MyException
解决方案 14:
我喜欢的一种方法是基于观察者模式。我定义了一个信号类,我的线程使用它来向侦听器发出异常。它还可以用于从线程返回值。示例:
import threading
class Signal:
def __init__(self):
self._subscribers = list()
def emit(self, *args, **kwargs):
for func in self._subscribers:
func(*args, **kwargs)
def connect(self, func):
self._subscribers.append(func)
def disconnect(self, func):
try:
self._subscribers.remove(func)
except ValueError:
raise ValueError('Function {0} not removed from {1}'.format(func, self))
class WorkerThread(threading.Thread):
def __init__(self, *args, **kwargs):
super(WorkerThread, self).__init__(*args, **kwargs)
self.Exception = Signal()
self.Result = Signal()
def run(self):
if self._Thread__target is not None:
try:
self._return_value = self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
except Exception as e:
self.Exception.emit(e)
else:
self.Result.emit(self._return_value)
if __name__ == '__main__':
import time
def handle_exception(exc):
print exc.message
def handle_result(res):
print res
def a():
time.sleep(1)
raise IOError('a failed')
def b():
time.sleep(2)
return 'b returns'
t = WorkerThread(target=a)
t2 = WorkerThread(target=b)
t.Exception.connect(handle_exception)
t2.Result.connect(handle_result)
t.start()
t2.start()
print 'Threads started'
t.join()
t2.join()
print 'Done'
我没有足够的线程使用经验来声称这是一种完全安全的方法。但它对我来说很有效,我喜欢它的灵活性。
解决方案 15:
捕获线程异常并与调用者方法通信的一个简单方法是通过将字典或列表传递给worker
方法。
示例(将字典传递给工作方法):
import threading
def my_method(throw_me):
raise Exception(throw_me)
def worker(shared_obj, *args, **kwargs):
try:
shared_obj['target'](*args, **kwargs)
except Exception as err:
shared_obj['err'] = err
shared_obj = {'err':'', 'target': my_method}
throw_me = "Test"
th = threading.Thread(target=worker, args=(shared_obj, throw_me), kwargs={})
th.start()
th.join()
if shared_obj['err']:
print(">>%s" % shared_obj['err'])
解决方案 16:
用异常存储包装线程。
import threading
import sys
class ExcThread(threading.Thread):
def __init__(self, target, args = None):
self.args = args if args else []
self.target = target
self.exc = None
threading.Thread.__init__(self)
def run(self):
try:
self.target(*self.args)
raise Exception('An error occured here.')
except Exception:
self.exc=sys.exc_info()
def main():
def hello(name):
print(!"Hello, {name}!")
thread_obj = ExcThread(target=hello, args=("Jack"))
thread_obj.start()
thread_obj.join()
exc = thread_obj.exc
if exc:
exc_type, exc_obj, exc_trace = exc
print(exc_type, ':',exc_obj, ":", exc_trace)
main()
解决方案 17:
我喜欢这门课:
https://gist.github.com/earonesty/b88d60cb256b71443e42c4f1d949163e
import threading
from typing import Any
class PropagatingThread(threading.Thread):
"""A Threading Class that raises errors it caught, and returns the return value of the target on join."""
def __init__(self, *args, **kwargs):
self._target = None
self._args = ()
self._kwargs = {}
super().__init__(*args, **kwargs)
self.exception = None
self.return_value = None
assert self._target
def run(self):
"""Don't override this if you want the behavior of this class, use target instead."""
try:
if self._target:
self.return_value = self._target(*self._args, **self._kwargs)
except Exception as e:
self.exception = e
finally:
# see super().run() for why this is necessary
del self._target, self._args, self._kwargs
def join(self, timeout=None) -> Any:
super().join(timeout)
if self.exception:
raise self.exception
return self.return_value
解决方案 18:
正如前面提到的,ThreadPoolExecutor 可以解决这个问题。
不过,也值得一提的是异常钩子。Jason Brownlee 对它进行了很好的描述。
注意:此钩子功能适用于 Python >= 3.8
以下是 Jason 的代码片段
# SuperFastPython.com
# example of an unhandled exception in a thread
from time import sleep
import threading
# target function that raises an exception
def work():
print('Working...')
sleep(1)
# rise an exception
raise Exception('Something bad happened')
# custom exception hook
def custom_hook(args):
# report the failure
print(f'Thread failed: {args.exc_value}')
# set the exception hook
threading.excepthook = custom_hook
# create a thread
thread = threading.Thread(target=work)
# run the thread
thread.start()
# wait for the thread to finish
thread.join()
# continue on
print('Continuing on...')
值得一提的是,threading.excepthook 应谨慎使用。如果您在较大的应用程序或库中工作,则挂钩全局线程异常处理程序可能会无意中影响应用程序的其他部分。
解决方案 19:
使用赤裸裸的例外并不是一个好的做法,因为你通常会遇到比你预期更多的事情。
我建议修改except
以仅捕获您想要处理的异常。我不认为引发它会产生预期的效果,因为当您TheThread
在外部实例化时try
,如果它引发异常,则分配永远不会发生。
相反,你可能只想对此发出警报并继续前进,例如:
def run(self):
try:
shul.copytree(self.sourceFolder, self.destFolder)
except OSError, err:
print err
然后,当捕获到该异常时,您可以在那里处理它。然后,当外部try
捕获异常时TheThread
,您知道它不会是您已经处理过的异常,并且将帮助您隔离流程。
解决方案 20:
我认为,如果您唯一想要的只是真正看到某个地方的异常而不是完全视而不见,那么其他解决方案会有些复杂。
解决方案是创建一个自定义的Thread
,从主线程获取记录器并记录任何异常。
class ThreadWithLoggedException(threading.Thread):
"""
Similar to Thread but will log exceptions to passed logger.
Args:
logger: Logger instance used to log any exception in child thread
Exception is also reachable via <thread>.exception from the main thread.
"""
def __init__(self, *args, **kwargs):
try:
self.logger = kwargs.pop("logger")
except KeyError:
raise Exception("Missing 'logger' in kwargs")
super().__init__(*args, **kwargs)
self.exception = None
def run(self):
try:
if self._target is not None:
self._target(*self._args, **self._kwargs)
except Exception as exception:
thread = threading.current_thread()
self.exception = exception
self.logger.exception(f"Exception in child thread {thread}: {exception}")
finally:
del self._target, self._args, self._kwargs
例子:
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())
def serve():
raise Exception("Earth exploded.")
th = ThreadWithLoggedException(target=serve, logger=logger)
th.start()
主线程中的输出:
Exception in child thread <ThreadWithLoggedException(Thread-1, started 139922384414464)>: Earth exploded.
Traceback (most recent call last):
File "/core/utils.py", line 108, in run
self._target(*self._args, **self._kwargs)
File "/myapp.py", line 105, in serve
raise Exception("Earth exploded.")
Exception: Earth exploded.
解决方案 21:
使用尝试闭包包装传递给线程的函数:
def exception_printing(f):
def f_(*args):
try:
return f(*args)
except Exception as e:
print(e)
return None
return f_
解决方案 22:
pygolang提供了sync.WorkGroup,它可以将生成的工作线程中的异常传播到主线程。例如:
#!/usr/bin/env python
"""This program demostrates how with sync.WorkGroup an exception raised in
spawned thread is propagated into main thread which spawned the worker."""
from __future__ import print_function
from golang import sync, context
def T1(ctx, *argv):
print('T1: run ... %r' % (argv,))
raise RuntimeError('T1: problem')
def T2(ctx):
print('T2: ran ok')
def main():
wg = sync.WorkGroup(context.background())
wg.go(T1, [1,2,3])
wg.go(T2)
try:
wg.wait()
except Exception as e:
print('Tmain: caught exception: %r
' %e)
# reraising to see full traceback
raise
if __name__ == '__main__':
main()
运行时给出以下内容:
T1: run ... ([1, 2, 3],)
T2: ran ok
Tmain: caught exception: RuntimeError('T1: problem',)
Traceback (most recent call last):
File "./x.py", line 28, in <module>
main()
File "./x.py", line 21, in main
wg.wait()
File "golang/_sync.pyx", line 198, in golang._sync.PyWorkGroup.wait
pyerr_reraise(pyerr)
File "golang/_sync.pyx", line 178, in golang._sync.PyWorkGroup.go.pyrunf
f(pywg._pyctx, *argv, **kw)
File "./x.py", line 10, in T1
raise RuntimeError('T1: problem')
RuntimeError: T1: problem
问题的原始代码如下:
wg = sync.WorkGroup(context.background())
def _(ctx):
shul.copytree(sourceFolder, destFolder)
wg.go(_)
# waits for spawned worker to complete and, on error, reraises
# its exception on the main thread.
wg.wait()