(注意:这是一个过于简化的场景,用于演示我的编码问题。)
我有以下类接口:
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中)进行一次调用。因此,我需要对公共方法的所有调用等待相同的内部任务。
回顾一下,这是我需要弄清楚如何完成的事情:
对FindCustomersInArea和GetCustomerByName的任何调用都应返回一个等待LoadCustomersAsync方法完成的Task。如果LoadCustomersAsync已经返回(并且缓存的列表仍然有效),则该方法可以立即继续。
在LoadCustomersAsync返回后,每个方法都需要额外的逻辑才能将列表转换为方法的所需返回值。
只能对LoadCustomersAsync(其中的GetAsync方法)进行一次活动调用。
如果缓存列表过期,则后续调用将触发重新加载(通过LoadCustomersAsync)。
如果您需要进一步澄清,请告诉我,但我希望这是一个足够普遍的用例,有人可以帮助我制定逻辑,让客户按需工作。
答案 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;
}
ContinueWith
添加到任务的任何延续。 ContinueWith
添加到任务的任何延续。注意:我在这里使用了粗粒度锁定方法,理论上你可以通过读写器实现来提高性能,但在你的情况下它可能是一个微优化。