Задача кеширования - PullRequest
       42

Задача кеширования

1 голос
/ 22 сентября 2019

Дано: сервер, который может делать запросы на обслуживание.Сервис может выполнить только один запрос за раз.Я хочу кэшировать запущенную задачу для обслуживания, например, если сервер выполняет запрос для первого клиента и получает запрос от второго клиента, сервер вернет уже созданную задачу второму клиенту вместо ожидания завершения первой задачи и запуска второго запроса.

Первая версия была с замками:

public class Lock
{
    private readonly object _lock = new object();
    private Task<long> _task;

    public Task<long> GetServiceResultAsync()
    {
        async Task<long> GetTaskAsync()
        {
            // to execute lock scope very fast
            await Task.Yield();

            return await GetServiceResultInternalAsync();
        }

        lock (_lock)
        {
            if (_task == null ||
                _task.IsCompleted)
            {
                _task = GetTaskAsync();
                _task.ContinueWith(task =>
                {
                    lock (_lock)
                    {
                        _task = null;
                    }
                });
            }

            return _task;
        }
    }

    private async Task<long> GetServiceResultInternalAsync()
    {
        await Task.Delay(1000);
        return DateTime.Now.Ticks;
    }
}

Но она не выглядела хорошо для меня.Поэтому я переписал его с помощью Interlocked:

public class Interlock
{
    private Lazy<Task<long>> _task;

    public Task<long> GetServiceResultAsync()
    {
        return TaskCacheHelper.GetOrCreateTask(ref _task, GetServiceResultInternalAsync, NullifyStartUpgradeTask);
    }

    private void NullifyStartUpgradeTask(Lazy<Task<long>> lazy)
    {
        Interlocked.CompareExchange(ref _task, null, lazy);
    }

    private async Task<long> GetServiceResultInternalAsync()
    {
        await Task.Delay(1000);
        return DateTime.Now.Ticks;
    }
}

public static class TaskCacheHelper
{
    public static Task<T> GetOrCreateTask<T>(ref Lazy<Task<T>> field, Func<Task<T>> getTask, Action<Lazy<Task<T>>> nullifyField)
    {
        var oldField = field;
        if (oldField != null && !oldField.Value.IsCompleted)
        {
            return oldField.Value;
        }

        // use Lazy to prevent task from executing 2 times 
        var newValue = new Lazy<Task<T>>(getTask);
        var originalFieldValue = Interlocked.CompareExchange(ref field, newValue, oldField);

        if (originalFieldValue == oldField)
        {
            // external delegate used here because ref parameter can't be used inside lambda
            newValue.Value.ContinueWith(task => nullifyField(newValue));

            return newValue.Value;
        }

        return originalFieldValue.Value;
    }
}

Вторая версия выглядит лучше, но я запутался в методе Nullify: это обычная логика, но ее следует писать каждый раз.

Итак,вопросы:

  • есть ли способ поместить NullifyStartUpgradeTask в TaskCacheHelper, ig повторно использовать его и не писать для каждого использования (потому что оно подвержено ошибкам)?
  • может быть, лучшерешение?

Использование:

static async Task Main(string[] args)
{
    var pr = new Interlock();
    var t1 = pr.GetServiceResultAsync();
    var t2 = pr.GetServiceResultAsync();

    await Task.WhenAll(t1, t2);
    Debug.Assert(t1.Result == t2.Result);

    var t3 = await pr.GetServiceResultAsync();
    var t4 = await pr.GetServiceResultAsync();
    Debug.Assert(t3 != t4);
}
...