Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ensuring that only one instance of a function is running?

I'm just getting into concurrent programming. Most probably my issue is very common, but since I can't find a good name for it, I can't google it.

I have a C++ UWP application where I try to apply MVVM pattern, but I guess that the pattern or even being UWP is not relevant.

First, I have a service interface that exposes an operation:

struct IService
{
    virtual task<int> Operation() = 0;
};

Of course, I provide a concrete implementation, but it is not relevant for this discussion. The operation is potentially long-running: it makes an HTTP request.

Then I have a class that uses the service (again, irrelevant details omitted):

class ViewModel
{
    unique_ptr<IService> service;
public:
    task<void> Refresh();
};

I use coroutines:

task<void> ViewModel::Refresh()
{
    auto result = co_await service->Operation();
    // use result to update UI
}

The Refresh function is invoked on timer every minute, or in response to a user request. What I want is: if a Refresh operation is already in progress when a new one is started or requested, then abandon the second one and just wait for the first one to finish (or time out). In other words, I don't want to queue all the calls to Refresh - if a call is already in progress, I prefer to skip a call until the next timer tick.

My attempt (probably very naive) was:

mutex refresh;
task<void> ViewModel::Refresh()
{
    unique_lock<mutex> lock(refresh, try_to_lock);
    if (!lock)
    {
        // lock.release(); commented out as harmless but useless => irrelevant
        co_return;
    }
    auto result = co_await service->Operation();
    // use result to update UI
}

Edit after the original post: I commented out the line in the code snippet above, as it makes no difference. The issue is still the same.

But of course an assertion fails: unlock of unowned mutex. I guess that the problem is the unlock of mutex by unique_lock destructor, which happens in the continuation of the coroutine and on a different thread (other than the one it was originally locked on).

Using Visual C++ 2017.

like image 592
Janis Norvelis Avatar asked Nov 07 '22 22:11

Janis Norvelis


1 Answers

use std::atomic_bool:

std::atomic_bool isRunning = false;
if (isRunning.exchange(true, std::memory_order_acq_rel) == false){
   try{
    auto result = co_await Refresh();
    isRunning.store(false, std::memory_order_release);
    //use result 
   }
   catch(...){
     isRunning.store(false, std::memory_order_release);
     throw;
   }
}

Two possible improvements : wrap isRunning.store in a RAII class and use std::shared_ptr<std::atomic_bool> if the lifetime if the atomic_bool is scoped.

like image 108
David Haim Avatar answered Nov 15 '22 06:11

David Haim