StackOverflower
StackOverflower

Reputation: 5761

How to make different calls to an async method waiting for the result of first call?

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.

Upvotes: 3

Views: 310

Answers (2)

Markeli
Markeli

Reputation: 558

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();
        }
    }

Upvotes: 0

Stuart
Stuart

Reputation: 5496

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 =)

Upvotes: 3

Related Questions