Can a Task have multiple awaiters?

2019-01-26 07:29发布

问题:

I am toying around with an async service for a Windows 8 project and there are some async calls of this service, which should only be called once at a time.

 public async Task CallThisOnlyOnce()
 {
      PropagateSomeEvents();

      await SomeOtherMethod();

      PropagateDifferentEvents();
 }

Since you cannot encapsulate an async call in a lock statement, i thought of using the AsyncLock pattern, but than i thought i might as well try something like this:

 private Task _callThisOnlyOnce;
 public Task CallThisOnlyOnce()
 {
      if(_callThisOnlyOnce != null && _callThisOnlyOnce.IsCompleted)
         _callThisOnlyOnce = null;

      if(_callThisOnlyOnce == null)
         _callThisOnlyOnce = CallThisOnlyOnceAsync();

      return _callThisOnlyOnce;
 }

 private async Task CallThisOnlyOnceAsync()
 {
      PropagateSomeEvents();

      await SomeOtherMethod();

      PropagateDifferentEvents();
 }

Therefore you would end up with the call CallThisOnlyOnceAsync only executed once simultanously, and multiple awaiters hooked on the same Task.

Is this a "valid" way of doing this or are there some drawbacks to this approach?

回答1:

A task can have multiple awaiters. However, as Damien pointed out, there's serious race conditions with your proposed code.

If you want the code executed each time your method is called (but not simultaneously), then use AsyncLock. If you want the code executed only once, then use AsyncLazy.

Your proposed solution attempts to combine multiple calls, executing the code again if it is not already running. This is more tricky, and the solution heavily depends on the exact semantics you need. Here's one option:

private AsyncLock mutex = new AsyncLock();
private Task executing;

public async Task CallThisOnlyOnceAsync()
{
  Task action = null;
  using (await mutex.LockAsync())
  {
    if (executing == null)
      executing = DoCallThisOnlyOnceAsync();
    action = executing;
  }

  await action;
}

private async Task DoCallThisOnlyOnceAsync()
{
  PropagateSomeEvents();

  await SomeOtherMethod();

  PropagateDifferentEvents();

  using (await mutex.LockAsync())
  {
    executing = null;
  }
}

It's also possible to do this with Interlocked, but that code gets ugly.

P.S. I have AsyncLock, AsyncLazy, and other async-ready primitives in my AsyncEx library.



回答2:

This code looks very "racy" if multiple threads might be involved.

One example (I'm sure there are more). Assume that _callThisOnlyOnce is currently null:

Thread 1                                                          Thread 2

public Task CallThisOnlyOnce()
{
  if(_callThisOnlyOnce != null && _callThisOnlyOnce.IsCompleted)
     _callThisOnlyOnce = null;

  if(_callThisOnlyOnce == null)
                                                                   public Task CallThisOnlyOnce()
                                                                   {
                                                                     if(_callThisOnlyOnce != null && _callThisOnlyOnce.IsCompleted)
                                                                        _callThisOnlyOnce = null;

                                                                     if(_callThisOnlyOnce == null)
                                                                        _callThisOnlyOnce = CallThisOnlyOnceAsync();

                                                                     return _callThisOnlyOnce;
                                                                   }
     _callThisOnlyOnce = CallThisOnlyOnceAsync();

  return _callThisOnlyOnce;
}

You now have 2 calls running simultaneously.

As to multiple awaiters, yes you can do this. I'm sure I've seen example code from MS somewhere showing an optimization where e.g. the result of Task.FromResult(0) is stored away in a static member and returned any time the function wants to return zero.

I have, however, been unsuccessful in locating this code sample.