C# asyn c lock получить тот же результат без выполнения кода - PullRequest
0 голосов
/ 07 мая 2020

У меня есть метод, который возвращает некоторое значение на основе вызова API, этот API ограничивает количество вызовов, которые вы можете сделать за период времени. Мне нужно получить доступ к результатам этого вызова из нескольких потоков. Прямо сейчас у меня есть следующий код:

class ReturningSemaphoreLocker<TOutput>
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task<T> LockAsync<T>(Func<Task<T>> worker)
    {
        await _semaphore.WaitAsync();
        try
        {
            return await worker();
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

Пример использования:

...
private static readonly ReturningSemaphoreLocker<List<int>> LockingSemaphore = new ReturningSemaphoreLocker<List<int>>();
...
public async Task<List<int>> GetStuff()
{
    return await LockingSemaphore.LockAsync(async () =>
    {
        var client = _clientFactory.CreateClient("SomeName");

        using (var cts = GetDefaultRequestCts())
        {
            var resp = await client.GetAsync("API TO QUERY URL", cts.Token);

            var jsonString = await resp.Content.ReadAsStringAsync();

            var items = JsonConvert.DeserializeObject<List<int>>(jsonString);

            return items;
        }
    });
}

Итак, вопрос: как получить тот же результат от GetStuff(), если он уже запущен БЕЗ повторного запроса API и повторного запроса API, если метод не запущен в данный момент?

Ответы [ 2 ]

2 голосов
/ 07 мая 2020

Уловка здесь состоит в том, чтобы удерживать Task<T>, который является неполным результатом; рассмотрим следующий полностью непроверенный подход - ключевым здесь является поле _inProgress:

private static readonly ReturningSemaphoreLocker<List<int>> LockingSemaphore = new ReturningSemaphoreLocker<List<int>>();
...
private Task<List<int>> _inProgress;
public Task<List<int>> GetStuffAsync()
{
    if (_inProgress != null) return _inProgress;
    return _inProgress = GetStuffImplAsync();
}
private async Task<List<int>> GetStuffImplAsync()
{
    var result = await LockingSemaphore.LockAsync(async () =>
    {
        var client = _clientFactory.CreateClient("SomeName");

        using (var cts = GetDefaultRequestCts())
        {
            var resp = await client.GetAsync("API TO QUERY URL", cts.Token);

            var jsonString = await resp.Content.ReadAsStringAsync();

            var items = JsonConvert.DeserializeObject<List<int>>(jsonString);

            return items;
        }
    });
    // this is important so that if everything turns
    // out to be synchronous, we don't nuke the _inProgress field *before*
    // it has actually been set
    await Task.Yield();

    // and now wipe the field since we know it is no longer in progress;
    // the next caller should actually try to do something interesting
    _inProgress = null;

    return result;
}
0 голосов
/ 08 мая 2020

Вот класс, который вы могли бы использовать для регулирования по времени вместо ReturningSemaphoreLocker:

class ThrottledOperation
{
    private readonly object _locker = new object();
    private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
    private Task _task;

    public Task<T> GetValueAsync<T>(Func<Task<T>> taskFactory, TimeSpan interval)
    {
        lock (_locker)
        {
            if (_task != null && (_stopwatch.Elapsed < interval || !_task.IsCompleted))
            {
                return (Task<T>)_task;
            }
            _task = taskFactory();
            _stopwatch.Restart();
            return (Task<T>)_task;
        }
    }
}

Метод GetValueAsync возвращает ту же задачу, пока не истечет интервал регулирования. и задача выполнена. В этот момент он создает и возвращает новую задачу, используя предоставленный метод фабрики задач.

Пример использования:

private static readonly ThrottledOperation _throttledStuff = new ThrottledOperation();

public Task<List<int>> GetStuffAsync()
{
    return _throttledStuff.GetValueAsync(async () =>
    {
        var client = _clientFactory.CreateClient("SomeName");
        using (var cts = GetDefaultRequestCts())
        {
            var resp = await client.GetAsync("API TO QUERY URL", cts.Token);
            var jsonString = await resp.Content.ReadAsStringAsync();
            var items = JsonConvert.DeserializeObject<List<int>>(jsonString);
            return items;
        }
    }, TimeSpan.FromSeconds(30));
}
...