Несколько вызовов ожидают выполнения одной и той же внутренней асинхронной задачи - PullRequest
2 голосов
/ 04 марта 2012

(Примечание: это слишком упрощенный сценарий, демонстрирующий мою проблему кодирования.)

У меня есть следующий интерфейс класса:

public class CustomerService
{
    Task<IEnumerable<Customer>> FindCustomersInArea(String areaName);
    Task<Customer> GetCustomerByName(String name);
    :
}

Это клиентская частьRESTful API, который загружает список объектов Customer с сервера, затем предоставляет методы, позволяющие клиентскому коду использовать и работать с этим списком.

Оба эти метода работают с внутренним списком клиентов, полученным с сервера, следующим образом:

private Task<IEnumerable<Customer>> LoadCustomersAsync()
{
    var tcs = new TaskCompletionSource<IEnumerable<Customer>>();

    try
    {
        // GetAsync returns Task<HttpResponseMessage>
        Client.GetAsync(uri).ContinueWith(task =>
        {
            if (task.IsCanceled)
            {
                tcs.SetCanceled();
            }
            else if (task.IsFaulted)
            {
                tcs.SetException(task.Exception);
            }
            else
            {
                // Convert HttpResponseMessage to desired return type
                var response = task.Result;

                var list = response.Content.ReadAs<IEnumerable<Customer>>();

                tcs.SetResult(list);
            }
        });
    }
    catch (Exception ex)
    {
        tcs.SetException(ex);
    }
}

Класс Client - это пользовательская версия класса HttpClient из веб-API WCF (теперь ASP.NET Web API), потому что я работаю в Silverlight, и у них нет SL-версииих клиентские сборки.

После всего этого, вот моя проблема:

Все методы в классе CustomerService используют список, возвращаемый асинхронным методом LoadCustomersAsync;поэтому любые вызовы этих методов должны ждать (асинхронно), пока метод LoadCustomers не вернется и логика appopriate будет выполнена в возвращенном списке.

Я также хочу, чтобы только один вызов был сделан из клиента (в LoadCustomers) ввремя.Итак, мне нужно, чтобы все вызовы открытых методов ожидали выполнения одной и той же внутренней задачи.

Чтобы рассмотреть, вот что мне нужно, чтобы выяснить, как выполнить:

  1. Любой вызов FindCustomersInArea и GetCustomerByName должен возвращать задачу, ожидающую завершения метода LoadCustomersAsync.Если LoadCustomersAsync уже вернулось (и кэшированный список все еще действителен), то метод может продолжиться немедленно.

  2. После возврата LoadCustomersAsync у каждого метода есть дополнительная логика, необходимая для преобразования списка в требуемыйвозвращаемое значение для метода.

  3. Всегда должен быть только один активный вызов LoadCustomersAsync (внутри метода GetAsync).

  4. Еслисрок действия кэшированного списка истекает, затем последующие вызовы вызовут перезагрузку (через LoadCustomersAsync).

Дайте мне знать, если вам нужны дополнительные разъяснения, но я надеюсь, что это достаточно распространенный вариант использования, которыйкто-то может помочь мне проработать логику, чтобы заставить клиента работать так, как нужно.

Ответы [ 2 ]

1 голос
/ 05 марта 2012

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


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

Вместо того, чтобы каждый раз создавать / возвращать новый TaskCompletionSource<T> (TCS) из метода LoadCustomerAsync, вместо этого у вас будет поле в классе, представляющем TCS.Это позволит вашему экземпляру запомнить TCS, который в последний раз представлял вызов, который представлял пропущенный кеш.Это состояние TCS будет сигнализироваться точно так же, как ваш существующий код.Вы добавите сведения о том, истек ли срок действия данных, как другое поле, которое в сочетании с тем, является ли TCS в настоящее время нулевым или нет, будет инициировать, действительно ли вы выходите и загружаете данные снова.

Хорошо, хватит разговоров, возможно, будет гораздо понятнее, если вы его увидите.

Код

public class CustomerService 
{ 
    // Your cache timeout (using 15mins as example, can load from config or wherever)
    private static readonly TimeSpan CustomersCacheTimeout = new TimeSpan(0, 15, 0);

    // A lock object used to provide thread safety
    private object loadCustomersLock = new object();
    private TaskCompletionSource<IEnumerable<Customer>> loadCustomersTaskCompletionSource;
    private DateTime loadCustomersLastCacheTime = DateTime.MinValue;

    private Task<IEnumerable<Customer>> LoadCustomersAsync()
    {
        lock(this.loadCustomersLock)
        {
            bool needToLoadCustomers = this.loadCustomersTaskCompletionSource == null
                                             ||
                                       (this.loadCustomersTaskCompletionSource.Task.IsFaulted || this.loadCustomersTaskCompletionSource.Task.IsCanceled)
                                             ||
                                       DateTime.Now - this.loadCustomersLastCacheTime.Value > CustomersService.CustomersCacheTimeout;

            if(needToLoadCustomers)
            {
                this.loadCustomersTaskCompletionSource = new TaskCompletionSource<IEnumerable<Customer>>();

                try
                {
                     // GetAsync returns Task<HttpResponseMessage>
                     Client.GetAsync(uri).ContinueWith(antecedent =>
                     {
                        if(antecedent.IsCanceled)
                        {
                            this.loadCustomersTaskCompletionSource.SetCanceled();
                        }
                        else if(antecedent.IsFaulted)
                        {
                            this.loadCustomersTaskCompletionSource.SetException(antecedent.Exception);
                        }
                        else
                        {
                            // Convert HttpResponseMessage to desired return type
                            var response = antecedent.Result;

                            var list = response.Content.ReadAs<IEnumerable<Customer>>();

                            this.loadCustomersTaskCompletionSource.SetResult(list);

                            // Record the last cache time
                            this.loadCustomersLastCacheTime = DateTime.Now;
                        }
                    });
                }
                catch(Exception ex)
                {
                    this.loadCustomersTaskCompletionSource.SetException(ex);
                }
            }
        }
    }

    return this.loadCustomersTaskCompletionSource.Task; 
}

Сценарии, когда клиенты не загружены:

  1. Если это первый вызов, TCS будет нулевым, поэтому будет создан TCS и получены клиенты.
  2. Если предыдущий вызов был ошибочным или был отменен, будет создан новый TCSи клиенты получили.
  3. Если время ожидания кэша истекло, будет создан новый TCS, и клиенты будут выбраны.

Сценарии, в которых клиенты загружаются / загружаются:

  1. Если клиенты находятся в процессе загрузки , существующая задача TCS будет возвращенаи любые продолжения, добавленные к задаче с использованием ContinueWith, будут выполнены после того, как будет отправлен сигнал TCS.
  2. Если клиенты уже загружены , будет возвращена существующая задача TCS и все продолжениядобавление к задаче с использованием ContinueWith будет выполнено, как только планировщик сочтет нужным.

ПРИМЕЧАНИЕ: Я использовал здесь грубую настройку блокировки, и вы могли бы теоретически улучшить производительностьс реализацией читателя / писателя, но это, вероятно, будет микрооптимизацией в вашем случае.

1 голос
/ 04 марта 2012

Я думаю, вам следует изменить способ вызова Client.GetAsync (uri). Сделайте это примерно так:

Lazy<Task> getAsyncLazy = new Lazy<Task>(() => Client.GetAsync(uri));

А в вашем методе LoadCustomersAsync вы пишете:

getAsyncLazy.Value.ContinueWith(task => ...

Это гарантирует, что GetAsync будет вызываться только один раз, и что все, кто интересуется его результатом, получат одно и то же задание.

...