Skip to content

tf::Semaphore with exception may cause deadlocks in some situations #732

Open

Description

@olologin

Usage of tf::Semaphore with exception like below leads to a deadlock (where user always waits for task graph to finish), it happens because Semaphore keeps some of waiting tasks in its list, and the main task queue does not have those tasks.

void semaphore_complex1(int semaphore_size) {
 
    tf::Executor executor(semaphore_size * 4);
    tf::Taskflow taskflow;
 
    tf::Semaphore semaphore(semaphore_size);
 
    tf::Task init = taskflow.emplace([]() {});
    for (size_t i = 0; i < semaphore_size * 4; ++i)
    {
        tf::Task A = taskflow.emplace([]() {});
        tf::Task B = taskflow.emplace([]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            throw std::runtime_error("exception");
            });
        tf::Task C = taskflow.emplace([]() {});
        tf::Task D = taskflow.emplace([]() {});
 
        init.precede(A);
        A.precede(B);
        B.precede(C);
        C.precede(D);
 
        A.acquire(semaphore);
        D.release(semaphore);
    }
 
    REQUIRE(semaphore.value() == semaphore_size);
 
    auto future = executor.run(taskflow);
    auto fn = [&]() {
        auto res = future.wait_for(std::chrono::seconds(60));
        if (res == std::future_status::timeout)
        {
            FAIL("Timeout");
        }
        FAIL("No exception?!");
    };
 
    // when B throws the exception, D will not run and thus semaphore is not released
    REQUIRE_THROWS_WITH_AS(fn(), "exception", std::runtime_error);
 
    REQUIRE(semaphore.value() == 0);
 
    // reset the semaphore to a clean state before running the taskflow again
    semaphore.reset();
 
    REQUIRE(semaphore.value() == semaphore_size);
 
    // run it again
    REQUIRE_THROWS_WITH_AS(fn(), "exception", std::runtime_error);
}
 
TEST_CASE("Exception.Semaphore.Complex.4threads" * doctest::timeout(300)) {
    semaphore_complex1(4);
}

Activity

  1. aman-coder03 commented on Nov 28, 2025

    @aman-coder03

    Can I work on this issue?

  2. CalebLarsen commented on Dec 10, 2025

    @CalebLarsen

    I'm trying to think through solutions to this problem here. I would love someone who is more experienced to give their thoughts on the matter.

    In the taskflow shown above, when the exception is thrown, should the taskflow
    A. Cancel all of its work that it is trying to do
    or
    B. Find the cancelled nodes' semaphores that were to be released and do clean-up on them?

    I think A makes more sense. Again, I'm not an expert, but B feels fishy to me.

    If A is the way to go, how do you interrupt nodes that are waiting to acquire semaphores?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueGood for newcomershelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions