Skip to content

concurrent.futures.wait returns cancelled as "not done" #109934

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
cweberprosperitybh opened this issue Sep 26, 2023 · 1 comment
Open

concurrent.futures.wait returns cancelled as "not done" #109934

cweberprosperitybh opened this issue Sep 26, 2023 · 1 comment
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@cweberprosperitybh
Copy link

cweberprosperitybh commented Sep 26, 2023

Bug report

Bug description:

According to the documentation for concurrent.futures.wait, it:

Returns a named 2-tuple of sets. The first set, named done, contains the futures that completed (finished or cancelled futures) before the wait completed. The second set, named not_done, contains the futures that did not complete (pending or running futures).

In this example, however, it returns cancelled futures in not_done. Note the wait=True in the preceding executor.shutdown call, which should ensure futures are not cancelled between wait and print. task calls sleep to ensure at least one thread is pending when executor.shutdown is called, even on especially performant systems.

from concurrent.futures import ThreadPoolExecutor, wait
from time import sleep


def task():
    sleep(1)


executor = ThreadPoolExecutor(max_workers=1)
futures = [executor.submit(task), executor.submit(task)]
executor.shutdown(wait=True, cancel_futures=True)
wait_return = wait(futures, timeout=0)
print(wait_return.not_done)

Considerations

  • If we instead call wait with timeout=None, the script hangs indefinitely.
  • If we replace the call to executor.shutdown with futures[1].cancel() (leave timeout=0), the cancelled future is similarly included in not_done. We can add print(futures[1].cancelled()) to ensure the cancellation is successful before the call to wait.
  • Replacing both, however, produces the expected result.

I couldn't find an existing report; apologies if I missed it.

CPython versions tested on:

3.11

Operating systems tested on:

Linux, macOS

Linked PRs

@cweberprosperitybh cweberprosperitybh added the type-bug An unexpected behavior, bug, or error label Sep 26, 2023
@iritkatriel iritkatriel added the stdlib Python modules in the Lib dir label Nov 26, 2023
@duaneg
Copy link
Contributor

duaneg commented May 24, 2025

This appears to be a bug in concurrent.futures.ThreadPoolExecutor: when the pool is shutdown it cancels all pending work items, however it does not call set_running_or_notify_cancel on the future, so it stays in the CANCELLED state instead of being moved to the CANCELLED_AND_NOTIFIED state.

Aside from the task being incorrectly reported as "not done", this means code that is blocking waiting on the future to complete will never be awakened (e.g. if passing wait=True to shutdown, as the OP notes, or if using as_completed etc).

duaneg added a commit to duaneg/cpython that referenced this issue May 24, 2025
When `ThreadPoolExecutor` shuts down it cancels any pending futures, however at
present it doesn't notify waiters. Thus their state stays as `CANCELLED`
instead of `CANCELLED_AND_NOTIFIED` and any waiters are not awakened.

Call `set_running_or_notify_cancel` on the cancelled futures to fix this.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants