Как я могу заставить задачи ждать того же результата?

#c# #multithreading #async-await

#c# #многопоточность #асинхронное ожидание

Вопрос:

У меня есть простой одноэлементный класс,

 public class SimpleSingleton
{
    public async Task<int> GetRefreshedValue()
    {
        /*
            What goes here?
        */
        return await GetRefreshedValueImplementation();
        /*
            What goes here?
        */
    }

    private async Task<int> GetRefreshedValueImplementation()
    {
        /*
           Resource intensive and not thread safe
        */
    }
}
  

Поскольку это синглтон, GetRefreshedValue будет вызываться одновременно. Я хочу, чтобы одновременно выполнялась ровно одна или ноль задач GetRefreshedValueImplementation .

Само по себе это было бы просто, я мог бы использовать SemaphoreSlim .

 private static SemaphoreSlim gate = new SemaphoreSlim(1);
...
await gate.WaitAsync();
try
{
    return await GetRefreshedValueImplementation();
}
finally
{
    gate.Release();
}
  

Однако я хочу, чтобы каждая задача, ожидающая у входа, получала недавно вычисленное возвращаемое значение. Я не хочу, чтобы они стояли в очереди для выполнения вызова.

Каков наилучший способ написания этого кода?

Комментарии:

1. «недавно вычисленное возвращаемое значение» Что вы имеете в виду под этим? Вы хотите вычислить только один раз или у вас есть правило для пересчета?

2. @Bombinosh Я хочу, чтобы все абоненты, которые звонят после запуска внутренней задачи, ждали результата внутренней задачи вместе с исходным абонентом.

3. Рассматривали ли вы что-нибудь столь же простое, как использование System.Lazy<T> для хранения вычисленного значения?

4. @JohnWu Тогда он будет повторно использоваться вечно, а не только до завершения операции.

5. @JohnWu Да и AsyncLazy<T> но, как бы у меня был lazy, который длится только для вызова метода.

Ответ №1:

Итак, сама операция достаточно проста. Вам просто нужно сохранить Task для операции при ее запуске и очистить ее по завершении, чтобы вы могли повторно использовать задачу во время ее выполнения. Оттуда просто добавляется правильная синхронизация, чтобы ее можно было безопасно использовать из нескольких потоков (я предполагаю, что это необходимо, и что это не все проходит через один контекст синхронизации, если это так, вы можете удалить код блокировки.)

 public class Foo<T> //TODO come up with good name
{
    private Func<Task<T>> factory;
    private Task<T> currentInvocation;
    private object key = new object();
    public Foo(Func<Task<T>> factory)
    {
        this.factory = factory;
    }
    public Task<T> Value
    {
        get
        {
            lock (key)
            {
                if (currentInvocation == null)
                {
                    currentInvocation = factory();
                    currentInvocation?.ContinueWith(_ =>
                    {
                        lock (key) { currentInvocation = null; }
                    });
                }
                return currentInvocation;
            }
        }
    }
}
  

Ответ №2:

 public class SimpleSingleton
{
    private static Task<int> executingTask;
    private static object lockObject = new object();

    public async Task<int> GetRefreshedValue()
    {
        lock (lockObject)
            {
                if (executingTask == null || executingTask.IsCompleted)
                {
                    executingTask = GetRefreshedValueImplementation();
                }
            }
        return await executingTask;
    }

    private async Task<int> GetRefreshedValueImplementation()
    {
        /*
           Resource intensive and not thread safe
        */
    }
}
  

Ответ №3:

Насколько я понимаю в вашем случае, вам нужно разрешить вызовам получать результат одной и той же текущей задачи, и если их нет, следует создать новую. Если это так, то это послужит вашей цели:

 public class SimpleSingleton
{
    private SimpleSingleton() { }
    private static SimpleSingleton _instance;
    public static SimpleSingleton Instance => _instance ?? (_instance = new SimpleSingleton());
    public async Task<int> GetRefreshedValue()
    {
        return await GetRefreshedValueImplementation();
    }
    private volatile Task<int> _getRefreshedValueImplementationTask;
    private Task<int> GetRefreshedValueImplementation()
    {
        if (_getRefreshedValueImplementationTask is null || _getRefreshedValueImplementationTask.IsCompleted)
        {
            return _getRefreshedValueImplementationTask = Task.Run(async () =>
            {
                /*
                   Resource intensive and not thread safe
                */
                int r = new Random().Next(1000, 2001);
                await Task.Delay(r);
                return r;
            });
        }
        return _getRefreshedValueImplementationTask;
    }
}
  

Ответ №4:

Что-то вроде:

 public class SimpleSingleton
{
    private int _sequenceNo;
    private int _lastvalue;
    private object _lock = new object;

    public async Task<int> GetRefreshedValue()
    {
        var currentSeq = _sequenceNo;
        lock(_lock)
        {
           if (_sequenceNo == currentSeq)
           {
              _lastValue = await GetRefreshedValueImplementation();
              _sequenceNo  ;
           }
        }
        return _lastValue;
    }

    private async Task<int> GetRefreshedValueImplementation()
    {
        /*
           Resource intensive and not thread safe
        */
    }
}