bweber
bweber

Reputation: 4082

C++: std::async and std::mutex leads to deadlock on Linux but runs on Windows?

I just compiled a project I've been working on under Windows for Linux and found that it hangs at a certain point. Since I am using std::async and std::mutex my first assumption was, that it could be a deadlock problem. However, then I wonder why it runs fine on Windows. Here is the code:

void BorderExtractor::preprocessImageAsync(const PreprocessingSettings& settings) {
    _preprocessingMutex.lock();
    if (!_preprocessingActive) {
        _preprocessingActive = true;
        std::async(std::launch::async, &BorderExtractor::preprocessImage, this, settings);
        //this point is never reached on linux
        _preprocessingUpToDate = true;
    } else {
        _cachedSettings = settings;
        _preprocessingUpToDate = false;
    }
    _preprocessingMutex.unlock();
}

This is the function that never returns under Linux. It runs until the async call and then it just stops. It nearly appears as if the function wasn't launched asynchronously and the program waits for it to return, what wouldn't work, because the other function will try to lock the same mutex.

Here is the function that's called asynchronously:

void BorderExtractor::preprocessImage(PreprocessingSettings settings) {

    //here some image processing stuff is done

    _preprocessingMutex.lock();
    //this point is never reached on linux
    if (!_preprocessingUpToDate) {
        _preprocessingUpToDate = true;
        _preprocessingMutex.unlock();
        std::async(std::launch::async, &BorderExtractor::preprocessImage, this, _cachedSettings);
    } else {
        _preprocessingUpToDate = true;
        _preprocessingActive = false;
        _preprocessingMutex.unlock();
    }
}

The point after it tries to lock the mutex is never reached under linux.

Now, what is the problem? Is it my code that is faulty, or is there something special I have to pay attention to on Linux? (compiler flags, etc) To me this appears as if the async call is synchronous and thus causes a deadlock. But why should that be

Upvotes: 3

Views: 2722

Answers (1)

Andy Prowl
Andy Prowl

Reputation: 126462

This call:

async(std::launch::async, &BorderExtractor::preprocessImage, this, _cachedSettings);

Effectively runs synchronously. This is due to the fact that the destructor of an std::future returned by std::async() ends up joining with the asynchronous computation - notice, that the behavior would be different if you obtained the future in other ways.

Since you are not keeping the future object returned by std::async alive, its lifetime ends immediately after the function call returns, and its destructor blocks until the asynchronous computation terminates - which is forever, as this seems to cause a deadlock.

The reason why this works on Windows may be due to the fact that you are using a non-compliant implementation of the Standard Library (e.g. Microsoft's implementation that comes with VS2013), in which the future's destructor does not join with the asynchronous computation - MS did this intentionally, following the rationale illustrated in this (rejected) proposal by Herb Sutter.

If you are looking for a fire-and-forget approach, consider this alternative implementation of std::async(), which does not cause the returned future to block on destruction (courtesy of bamboon):

template<class Function, class... Args>
std::future<typename std::result_of<Function(Args...)>::type> async( 
    Function&& f, 
    Args&&... args) 
{
    using R = typename std::result_of<Function(Args...)>::type;
    auto bound_task = std::bind(std::forward<Function>(f), std::forward<Args>(args)...);
    auto task = std::packaged_task<R()>{std::move(bound_task)};
    auto ret = task.get_future();
    auto t = std::thread{std::move(task)};
    t.detach();
    return ret;   
}

As a side note, avoid explicitly locking/unlocking mutexes. Rather, use RAII wrappers like std::lock_guard or (if necessary) std::unique_lock to make sure your mutex will be unlocked even if an exception is thrown or in case of an early return:

// The mutex will be unlocked automatically when the function returns.
std::lock_guard<std::mutex> lock{_preprocessingMutex};

if (!_preprocessingUpToDate) {
    _preprocessingUpToDate = true;
    async(std::launch::async, &BorderExtractor::preprocessImage, this, _cachedSettings);
    // No need to call unlock() on the mutex!
} else {
    _preprocessingUpToDate = true;
    _preprocessingActive = false;
    // No need to call unlock() on the mutex!
}

Upvotes: 5

Related Questions