Home > Mobile >  Multiple threads awaiting Task if it is already running
Multiple threads awaiting Task if it is already running

Time:01-17

I have app which have different timers running in the background doing synchronization with the API.

However, before the actual sync they all ping the API whether they can proceed or not. In order to not make multiple calls, I've made them await the same Task if it is already running. After the result comes from the task those who await should get the result and in the mean time if there is a call to the CanExecute method and the Task is not running it should rerun the Task and so on.

protected async Task<bool> CanExecute([CallerMemberName] string memberName = "")
{
    lock (_lockobj)
    {
        if (_runningTask == null)
        {
           _runningTask = CanExecuteTask(memberName);
        }
    }

    var result = await _runningTask;
    _runningTask = null;
    return result;
}

private async Task<bool> CanExecuteTask(string callingMemberName)
{
    var result = // do http call and some other method calls
    return result;
}

I'm not sure what the problem is, but I think it happens to deadlock and the synchronization doesn't proceed. What is the proper way to achieve that?

CodePudding user response:

As written this code is not safe - you both read and write _runningTask outside of the lock. So for example, one thread can execute

_runningTask = null;

While another thread just got past the lock statement while task was not null and is about to execute await _runningTask. This will result in await null so in NullReferenceException.

You can avoid that while keeping the same principle by doing this:

private Task<bool> _runningTask = Task.FromResult(false);
protected async Task<bool> CanExecute([CallerMemberName] string memberName = "") {
    Task<bool> task;
    lock (_lockobj) {
        if (_runningTask.IsCompleted) {
            _runningTask = CanExecuteTask(memberName);
        }
        task = _runningTask;
    }

    var result = await task;        
    return result;
}

Instead of setting task to null and then checking that - we check if task has been completed (which means succeeded, faulted, or cancelled). We then also start with already completed task instead of null.

Also we assign _runningTask to local variable inside the lock, and then await that, to avoid reading _runningTask field outside of the lock.

Awaiting the same task from multiple threads is safe, so we should be fine here.

  •  Tags:  
  • c#
  • Related