3

Let's say I have a method it gets data from server

Task<Result> GetDataFromServerAsync(...)

If there is an ongoing call in progress, I don't want to start a new request to server but wait for the original to finish.

Let's say I have

var result = await objet.GetDataFromServerAsync(...);

and in a different place, called almost at the same time I have a second call

var result2 = await objet.GetDataFromServerAsync(...);

I don't want the second to start a new request to server if the first didn't finish. I want both calls to get the same result as soon as first call finish. This is a proof of concept, I have options but I wanted to see how easy it's to do this.

StackOverflower
  • 5,463
  • 13
  • 58
  • 89
  • does the second call run on a different thread? – Sam I am says Reinstate Monica Jan 17 '17 at 21:12
  • may be by making lets say, a manager, which is a gatekeeper to your server method calls it accepts multiple requests and do not pass them to server if they point to same resource on server, instead it requests that resource from server and then distributes the result to multiple tasks which requested the resource – Dipen Shah Jan 17 '17 at 21:12
  • 1
    Possible duplicate question of http://stackoverflow.com/questions/34439694/how-to-cache-data-as-long-as-task-executes. Easiest solution, get the [`Nito.AsyncEx`](https://www.nuget.org/packages/Nito.AsyncEx) package and use the premade [`AsyncLazy`](https://github.com/StephenCleary/AsyncEx/blob/master/Source/Nito.AsyncEx%20(NET45%2C%20Win8%2C%20WP8%2C%20WPA81)/AsyncLazy.cs) class – Scott Chamberlain Jan 17 '17 at 21:13
  • @SamIam: it doesn't run on the same thread – StackOverflower Jan 17 '17 at 21:28
  • @ScottChamberlain: I wanted to see if there was an easy way using standard .net framework. In this case it doesn't worth to add a new component just for this. Thanks for the tip anyway! – StackOverflower Jan 17 '17 at 21:29
  • The class is pretty simple, you could just copy and paste it right in to your code. Take out the debugger stuff, replace `TaskShim` with `Task`, and remove the properties you don't care about and you can get it down to like 20 or so lines of code. – Scott Chamberlain Jan 17 '17 at 21:31
  • @ScottChamberlain: thanks. I'll be exploring that component. Didn't have the chance yet but feel free to add that as an answer if you feel it's worth – StackOverflower Jan 17 '17 at 21:48

2 Answers2

3

Here is a quick example using Lazy<Task<T>>:

var lazyGetDataFromServer = new Lazy<Task<Result>>
    (() => objet.GetDataFromServerAsync(...));

var result  = await lazyGetDataFromServer.Value;
var result2 = await lazyGetDataFromServer.Value;

It doesn't matter if these 2 awaits are done from separate threads as Lazy<T> is thread-safe, so result2 if ran second will still wait and use the same output from result.

Using the code from here you can wrap this up in a class called AsyncLazy<T>, and add a custom GetAwaiter so that you can just await it without the need to do .Value, very tidy =)

Stuart
  • 5,358
  • 19
  • 28
0

You can use anything for syncrhonization in your method. For example, I used SemaphoreSlim:

public class PartyMaker
    {

        private bool _isProcessing;
        private readonly SemaphoreSlim _slowStuffSemaphore = new SemaphoreSlim(1, 1);
        private DateTime _something;


        public async Task<DateTime> ShakeItAsync()
        {
            try
            {
                var needNewRequest = !_isProcessing;
                await _slowStuffSemaphore.WaitAsync().ConfigureAwait(false);
                if (!needNewRequest) return _something;
                _isProcessing = true;

                _something = await ShakeItSlowlyAsync().ConfigureAwait(false);
                return _something;
            }
            finally
            {
                _isProcessing = false;
                _slowStuffSemaphore.Release();
            }
        }

        private async Task<DateTime> ShakeItSlowlyAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
            return DateTime.UtcNow;
        }
    }

Usage:

var maker = new PartyMaker();

        var tasks = new[] {maker.ShakeItAsync(), maker.ShakeItAsync()};

        Task.WaitAll(tasks);
        foreach (var task in tasks)
        {
            Console.WriteLine(task.Result);
        }
        Console.WriteLine(maker.ShakeItAsync().Result);

Here is result:

17.01.2017 22:28:39
17.01.2017 22:28:39
17.01.2017 22:28:41

UPD With this modification you can call async methods with args:

public class PartyMaker
    {
        private readonly SemaphoreSlim _slowStuffSemaphore = new SemaphoreSlim(1, 1);

        private readonly ConcurrentDictionary<int, int> _requestCounts = new ConcurrentDictionary<int, int>();
        private readonly ConcurrentDictionary<int, DateTime> _cache = new ConcurrentDictionary<int, DateTime>();


        public async Task<DateTime> ShakeItAsync(Argument argument)
        {
            var key = argument.GetHashCode();
            DateTime result;
            try
            {
                if (!_requestCounts.ContainsKey(key))
                {
                    _requestCounts[key] = 1;
                }
                else
                {
                    ++_requestCounts[key];
                }
                var needNewRequest = _requestCounts[key] == 1;
                await _slowStuffSemaphore.WaitAsync().ConfigureAwait(false);
                if (!needNewRequest)
                {
                    _cache.TryGetValue(key, out result);
                    return result;
                }
                _cache.TryAdd(key, default(DateTime));

                result = await ShakeItSlowlyAsync().ConfigureAwait(false);
                _cache[key] = result;
                return result;
            }
            finally
            {
                _requestCounts[key]--;
                if (_requestCounts[key] == 0)
                {
                    int temp;
                    _requestCounts.TryRemove(key, out temp);
                    _cache.TryRemove(key, out result);
                }
                _slowStuffSemaphore.Release();
            }
        }

        private async Task<DateTime> ShakeItSlowlyAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
            return DateTime.UtcNow;
        }
    }

    public class Argument
    {
        public Argument(int value)
        {
            Value = value;
        }


        public int Value { get;  }

        public override int GetHashCode()
        {
            return Value.GetHashCode();
        }
    }
Markeli
  • 558
  • 4
  • 16