You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, if a task is already running on a thread of a worker, it will always run to completion/exception even if the work is no longer required (e.g., the future was explicitly cancelled). We usually say "you cannot interrupt a python thread", so Dask has no option here. However, you can set the thread state to exception using ctypes.pythonapi.PyThreadState_SetAsyncExc. Yes, this is a C-api call, but it is public and stable. Is this worth pursuing? It would allow not only early cancellation of unneeded work, but "interrupts" and timeouts from the worker itself, perhaps even better debugging if it can break deadlocks with a useful traceback.
(Note that the setting of an exception only has effect at the next python bytecode instruction, so some long-running C-level function or even wait(big) will not raise immediately)
On Mon, Apr 12, 2021 at 10:47 AM Martin Durant ***@***.***> wrote:
Currently, if a task is already running on a thread of a worker, it will
always run to completion/exception even if the work is no longer required
(e.g., the future was explicitly cancelled). We usually say "you cannot
interrupt a python thread", so Dask has no option here. However, you can
<https://blog.finxter.com/how-to-kill-a-thread-in-python/#Method_4_Using_ctypes_To_Raise_Exceptions_In_A_Thread>
set the thread state to exception using
ctypes.pythonapi.PyThreadState_SetAsyncExc. Yes, this is a C-api call,
but it is public and stable. Is this worth pursuing? It would allow not
only early cancellation of unneeded work, but "interrupts" and timeouts
from the worker itself, perhaps even better debugging if it can break
deadlocks with a useful traceback.
(Note that the setting of an exception only has effect at the next python
bytecode instruction, so some long-running C-level function or even
wait(big) will not raise immediately)
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#4694>, or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACKZTB7NVEHGBXUKJRXRRDTIMIYTANCNFSM42ZRYKOQ>
.
If the thread spawns subthreads, what happens? Are they closed automagically as well?
What if the compute thread is supposed to close external resources, e.g. a connection, does it have time to do so, e.g. will the __exit__ of a ctx manager be executed? IIUC yes but we should confirm
Whatever would happen on a normal exception, I think. So the working thread, if it was waiting on some other thread, would raise the exception normally. The main thread would not know that there was another thread around, so work may still happen there, unless the code in the task was well-written to pass an "I have reached an exception, please end" message to its child thread.
if the compute thread is supposed to close external resources, e.g. a connection, does it have time to do so
Yes, it should be the equivalent of meeting the exception at the current location in the code, so contexts would be respected
Well, the problem is that the exception may interrupt your code at any point. For example if you have:
try:
# some computation ...finally:
# on exit, finalize some IO etc.
then the exception could be raised in the middle of the finally block and some cleanup steps would be missed. It could even be raised inside a __exit__ for a context manager.
For a library like distributed that does a lot of cleanup in destructors/finalizers/etc., it sounds like a bad idea.
That kind of cleanup doesn't happen within a task execution, though, at least not by Dask - these threads only run the functions supplied by the user.
You are right, in general, that code associated with leaving a task would not run if the "kill" happened after areas where an exception might have been anticipated. There's probably no easy way to tell if this is the case for arbitrary user code.
You could do this the other way: expose an API for user code to know whether the current task should be abandoned, and let the user code call it as desired. That allows for safe cancellation. And/or expose something like a allow_async_cancellation parameter or function in a task.
You could do this the other way: expose an API for user code to know whether the current task should be abandoned, and let the user code call it as desired.
I think it's pretty unlikely we would get any buy-in on this; in fact, you can already do this by introspecting the worker from get_worker().
is it inside a free-running thread or a ThreadPoolExecutor thread?
The latter, as subclassed in distributed. The exception would be seceded threads (which are still launched the same way). If I remember, an actor thread runs on a executor with exactly one thread.
I mean like this
(distributed/_concurrent_futures_thread.py)
def_worker(executor_reference, work_queue):
try:
whileTrue:
try:
work_item=work_queue.get(block=True)
ifwork_itemisnotNone:
work_item.run()
# Delete references to object. See issue16284delwork_itemcontinueexecutor=executor_reference()
# Exit if:# - The interpreter is shutting down OR# - The executor that owns the worker has been collected OR# - The executor that owns the worker has been shutdown.if_shutdownorexecutorisNoneorexecutor._shutdown:
# Notice other workerswork_queue.put(None)
returndelexecutorexceptWorkerThreadInterrupt:
# thread interrupt outside of a taskpassexceptBaseException:
_base.LOGGER.critical("Exception in worker", exc_info=True)
Activity
mrocklin commentedon Apr 12, 2021
martindurant commentedon Apr 12, 2021
I'll make a PR with a version of the code linked above, so we can discuss it more there.
jrbourbeau commentedon Apr 12, 2021
Yeah this would be nice to be able to do using a stable, public API
mrocklin commentedon Apr 12, 2021
fjetter commentedon Apr 12, 2021
I have a few concerns
__exit__of a ctx manager be executed? IIUC yes but we should confirmmartindurant commentedon Apr 12, 2021
Whatever would happen on a normal exception, I think. So the working thread, if it was waiting on some other thread, would raise the exception normally. The main thread would not know that there was another thread around, so work may still happen there, unless the code in the task was well-written to pass an "I have reached an exception, please end" message to its child thread.
Yes, it should be the equivalent of meeting the exception at the current location in the code, so contexts would be respected
fjetter commentedon Apr 12, 2021
If that all works this way, that sounds great
pitrou commentedon Apr 12, 2021
Well, the problem is that the exception may interrupt your code at any point. For example if you have:
then the exception could be raised in the middle of the
finallyblock and some cleanup steps would be missed. It could even be raised inside a__exit__for a context manager.For a library like
distributedthat does a lot of cleanup in destructors/finalizers/etc., it sounds like a bad idea.martindurant commentedon Apr 12, 2021
That kind of cleanup doesn't happen within a task execution, though, at least not by Dask - these threads only run the functions supplied by the user.
You are right, in general, that code associated with leaving a task would not run if the "kill" happened after areas where an exception might have been anticipated. There's probably no easy way to tell if this is the case for arbitrary user code.
pitrou commentedon Apr 12, 2021
You could do this the other way: expose an API for user code to know whether the current task should be abandoned, and let the user code call it as desired. That allows for safe cancellation. And/or expose something like a
allow_async_cancellationparameter or function in a task.pitrou commentedon Apr 12, 2021
Oh, and is it inside a free-running thread or a
ThreadPoolExecutorthread?martindurant commentedon Apr 12, 2021
I think it's pretty unlikely we would get any buy-in on this; in fact, you can already do this by introspecting the worker from
get_worker().The latter, as subclassed in distributed. The exception would be seceded threads (which are still launched the same way). If I remember, an actor thread runs on a executor with exactly one thread.
pitrou commentedon Apr 12, 2021
If your async exception is raised inside the ThreadPoolExecutor bookkeeping code, you might get a deadlock or other issues.
martindurant commentedon Apr 12, 2021
Does any of
ThreadPoolExecutor's own code run within the threads it manages?pitrou commentedon Apr 12, 2021
Well, yes, so that threads are reused. Spawning a new thread for each task would be wasteful.
https://github.com/python/cpython/blob/master/Lib/concurrent/futures/thread.py#L74-L101
martindurant commentedon Apr 12, 2021
Oh, right, I see it.
We can guard this by using our own exception class, right?
pitrou commentedon Apr 12, 2021
I'm not sure that would change anything. You could still interrupt the worker's task handling loop at any point.
martindurant commentedon Apr 12, 2021
I mean like this
(distributed/_concurrent_futures_thread.py)
pitrou commentedon Apr 12, 2021
Ah! Well, I suppose that could work...