让多个调用等待同一个内部异步任务

时间:2012-03-04 17:12:09

标签: .net-4.0 task task-parallel-library

(注意:这是一个过于简化的场景,用于演示我的编码问题。)

我有以下类接口:

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类是来自WCF Web API(现在是ASP.NET Web API)的HttpClient类的自定义版本,因为我在Silverlight中工作,并且他们没有客户端程序集的SL版本。

在完成所有背景后,这是我的问题:

CustomerService类中的所有方法都使用异步LoadCustomersAsync方法返回的列表;因此,对这些方法的任何调用都应等待(异步),直到LoadCustomers方法返回并在返回的列表上执行适当的逻辑。

我也只想要一次从客户端(在LoadCustomers中)进行一次调用。因此,我需要对公共方法的所有调用等待相同的内部任务。

回顾一下,这是我需要弄清楚如何完成的事情:

  1. 对FindCustomersInArea和GetCustomerByName的任何调用都应返回一个等待LoadCustomersAsync方法完成的Task。如果LoadCustomersAsync已经返回(并且缓存的列表仍然有效),则该方法可以立即继续。

  2. 在LoadCustomersAsync返回后,每个方法都需要额外的逻辑才能将列表转换为方法的所需返回值。

  3. 只能对LoadCustomersAsync(其中的GetAsync方法)进行一次活动调用。

  4. 如果缓存列表过期,则后续调用将触发重新加载(通过LoadCustomersAsync)。

  5. 如果您需要进一步澄清,请告诉我,但我希望这是一个足够普遍的用例,有人可以帮助我制定逻辑,让客户按需工作。

2 个答案:

答案 0 :(得分:1)

我认为你应该改变调用Client.GetAsync(uri)的方式。大致是这样的:

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

在你的LoadCustomersAsync方法中写下:

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

这将确保GetAsync仅被调用一次,并且对其结果感兴趣的每个人都将收到相同的任务。

答案 1 :(得分:1)

免责声明:我假设你正在使用你的HttpClient子类的单例实例。如果情况并非如此,我们只需稍微修改一下我要告诉你的内容。


是的,这完全可行。我们将要依赖于LoadCustomersAsync的后续调用的机制是,如果您将延续附加到Task,即使Task已完成之前已完成,您仍将继续随着任务的最终状态“立即”发出信号。

不是每次从TaskCompletionSource<T>方法创建/返回新的LoadCustomerAsync(TCS),而是在类上有一个代表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将为null,因此将创建TCS并获取客户。
  2. 如果上一次呼叫出现故障或被取消,将创建一个新的TCS并获取客户。
  3. 如果缓存超时已过期,将创建一个新的TCS并获取客户。
  4. 客户正在加载/加载的场景:

    1. 如果客户正在加载,则将返回现有TCS的任务,并且一旦发出TCS信号,将执行使用ContinueWith添加到任务的任何延续。
    2. 如果客户已加载,将返回现有TCS的任务,并且一旦调度程序认为合适,将立即执行使用ContinueWith添加到任务的任何延续。
    3. 注意:我在这里使用了粗粒度锁定方法,理论上你可以通过读写器实现来提高性能,但在你的情况下它可能是一个微优化。