8

Given the following function:

public async Task<int> GetValue() {
    var value = await GetValueFromServer();
    return value;
}

I would like to accomplish this. The function GetValue() can be called from different parts of the program. While GetValueFromServer() is running I don't want to initiate another call to it. All calls to to GetValue() while GetValueFromServer() is running should return the same value and not initiate another call. A call to GetValue() while GetValueFromServer() is not running should initiate a new call to GetValueFromServer().

An example:

0.0: var a = await GetValue()
0.1: call to GetValueFromServer()
0.3: var b = await GetValue();
0.6: var c = await GetValue();
0.7: GetValueFromServer() returns 1 
0.9: var d = await GetValue();
1.0: call to GetValueFromServer()
1.3 GetValueFromServer() returns 2

The calls to GetValue() at 0.0, 0.3 and 0.6 should only trigger a single call to GetValueFromServer(). All three callers should receive the same value. The call to GetValue() at 0.9 should trigger another call to GetValueFromServer().

I'm still stuck in Objective-C thinking where I would use blocks to accomplish this. I would queue up the blocks and when the request to the server returns call each block that was queued.

- (void)getValue:(void(^)(int value))block {
    [_runningBlocks addObject:[block copy]];
    if (_runningBlocks.count > 1) {
        return;
    }

    [self getValueFromServer:^(int value) {
        for (void(^)(int)b in _runningBlocks) {
           b(value);
        }
        [_runningBlocks removeAllObjects];
    ];
 }

Now I would like to accomplish the same thing but using the async/await pattern. This is for use in a Windows Phone 8.1 (RT) app and if possible I would like to avoid a third party library/framework.

Robert Höglund
  • 10,010
  • 13
  • 53
  • 70
  • I understand that you do not want to have multiple parallel task hitting the server with the same request, but looking at it from the consumers perspective, when will the value you already have from the last request get stale? Do you have to start a new request to the server on each request for the value from a consumer? – Alex Apr 17 '15 at 08:01
  • @Alex Good point. In this particular case it is not an issue but well worth remembering when using a pattern such as this. – Robert Höglund Apr 19 '15 at 15:50

1 Answers1

8

You need to share the same task in order to return the same value(You need to share the state). To do that, you need to synchronize the access.

Following should be more than enough.

private object padLock = new object();
private Task<int> executingTask;
public async Task<int> GetValue() 
{  
    lock(padLock)
    {
        if (executingTask  == null || executingTask.IsCompleted)
            executingTask= GetValueFromServer();
    }

    var value = await executingTask;
    return value;
}
Sriram Sakthivel
  • 72,067
  • 7
  • 111
  • 189
  • @Sriram The problem with this is that as soon as you do the `await getValueTask` you are waiting for the `getValueTask` to finish. So next time into `GetValue` `executingTask` will always be complete – Mike Norgate Apr 17 '15 at 07:47
  • 1
    @MikeNorgate: what "next time"? There can be concurrent calls into the `GetValue()` method, each of them awaiting the result. It is entirely plausible for a subsequent call to the method to occur before the task has completed. – Peter Duniho Apr 17 '15 at 07:49
  • 1
    @Peter maybe it's my misunderstanding of await. I thought await blocked the current execution until the task completes. I.e. in this example `value` will not have a value until the task has finished – Mike Norgate Apr 17 '15 at 07:52
  • @MikeNorgate: well, a) the whole point of `await` is that it does _not_ block execution, and b) even if it did (but I emphasize: it does not), the description of the OP's scenario would imply that there were multiple threads calling the method concurrently, so one thread being blocked wouldn't prevent a different thread from also calling it. – Peter Duniho Apr 17 '15 at 07:57
  • @SriramSakthivel Wouldn't you say that your precedent implementation (with an explicit semaphor) was somewhat better? It used `WaitAsync`, whereas `lock` is blocking the calling thread here. On the other hand, this one is more readable. – Patrice Gahide Apr 17 '15 at 08:09
  • 1
    @PatriceGahide It is not required actually. Blocking doesn't matter here, because we're calling an asynchronous method(GetValueFromServer), that shouldn't block. So, lock will not be taken for more than few nano seconds. It isn't a problem. OTOH semaphore is overkill here, because we aren't awaiting inside the lock. If we need to await inside lock, we need `SemaphoreSlim`. That's why I removed it from original answer. – Sriram Sakthivel Apr 17 '15 at 08:14
  • @SriramSakthivel It also depends on the number of concurrent threads that are trying to get the lock at the same time. A thread can wait for its turn a little more time than expected, even if each execution is very quick indeed. But you're right, this is probably overkill for most scenarios, if not all. – Patrice Gahide Apr 17 '15 at 08:20
  • 3
    @PatriceGahide .Net's implementation of lock isn't very expensive(no kernel object at first) if the lock is just for brief period of time. It will use "SpinWait" or so called "Busy-Wait" internally. If it takes more than a while, it will go back to kernel object. So, no context switch overheads etc. This is efficient than the semaphore version(is what I believe). – Sriram Sakthivel Apr 17 '15 at 08:23
  • 1
    @SriramSakthivel Interesting, and makes sense. Thanks for that. – Patrice Gahide Apr 17 '15 at 08:24
  • @SriramSakthivel There is a small chance to get an unobserved task result, the task might be completed and replaced by a new instance if the context switches before `await executingTask`. You can keep the reference in a local variable to avoid this situation or return the task inside the lock with no await. – Guillaume Apr 17 '15 at 08:31
  • @Guillaume Returning the original task may not be desired as it changes the [exception propagation behavior](http://stackoverflow.com/a/21082631/2530848), but good point. Well, I leave that local variable thing to OP. If you see the version history of my answer, I had it initially, after Peter's comments I removed it. – Sriram Sakthivel Apr 17 '15 at 08:35
  • I didn't know that calling await on the same Task twice would just return the same value without executing the method again. But it makes sense and it's really good to know (It also means I need to read up on async/await). Thank you for providing a good solution where I also learned a bunch. – Robert Höglund Apr 19 '15 at 16:07