拥有多个呼叫等待相同的内部异步任务 [英] Have multiple calls wait on the same internal async task

查看:169
本文介绍了拥有多个呼叫等待相同的内部异步任务的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

(注:这是一个过于简单化的方案来证明我的编码问题)

我有以下的类接口:

 公共类的CustomerService
{
    任务< IEnumerable的<客户>> FindCustomersInArea(字符串AREANAME);
    任务<客户> GetCustomerByName(字符串名称);
    :
}
 

这是一个RESTful API,它加载Customer对象的列表从服务器,然后公开的方法,可以让客户端code消耗和工作针对该名单的客户端。

这两种方法都对从服务器检索的顾客的内部列表工作如下:

 私人任务< IEnumerable的<客户>> LoadCustomersAsync()
{
    VAR TCS =新TaskCompletionSource< IEnumerable的<客户>>();

    尝试
    {
        // GetAsync返回任务< Htt的presponseMessage>
        Client.GetAsync(URI).ContinueWith(任务=>
        {
            如果(task.IsCanceled)
            {
                tcs.SetCanceled();
            }
            否则,如果(task.IsFaulted)
            {
                tcs.SetException(task.Exception);
            }
            其他
            {
                //转换的Htt presponseMessage到所需的返回类型
                VAR响应= task.Result;

                VAR列表= response.Content.ReadAs< IEnumerable的<客户>>();

                tcs.SetResult(名单);
            }
        });
    }
    赶上(例外前)
    {
        tcs.SetException(前);
    }
}
 

客户端类是从WCF的Web API(现在的ASP.NET Web API)HttpClient的类的自定义版本,因为我的工作在Silverlight和他们没有自己的客户端组件的SL版本。

毕竟这样的背景下,这是我的问题:

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

我也只想要一个电话从客户端进行(以LoadCustomers)的时间。所以,我需要的所有调用的公共方法,以等待相同的内部任务。

要检讨,这是我需要弄清楚如何做到:

  1. 任何调用FindCustomersInArea和GetCustomerByName应返回的任务等待LoadCustomersAsync方法来完成。如果LoadCustomersAsync已经返回(和缓存的列表仍有效),则该方法可继续立即

  2. LoadCustomersAsync返回后,每种方法都有到列表转换成该方法所需要的返回值需要额外的逻辑。

  3. 目前只能永远是LoadCustomersAsync一个激活呼叫(在GetAsync方法)。

  4. 如果缓存列表失效,那么后续调用将触发重载(通过LoadCustomersAsync)。

让我知道如果你需要进一步澄清,但我希望这是一个普遍不够用的情况下,有人可以帮我找出逻辑来获得所希望的客户端工作。

解决方案

免责声明:的我要你用你的HttpClient的子类的单一实例来承担。如果不是这种情况,我们只需要稍微修改什么,我要告诉你。


是的,这是完全可行的。我们要依靠后续调用机制 LoadCustomersAsync 是,如果您将延续到工作 ,即使工作完成亿万年前,你是继续将立即信号与任务的最终状态。

而不是创造/返回一个新的 TaskCompletionSource< T> (TCS)从 LoadCustomerAsync 方法每一次,你不是有一个字段上重新presents的TCS类。这将允许您的实例记住TCS,去年重新presented的重新presented高速缓存未命中的电话。这TCS的状态将被完全信号一样现有的code。您将添加的数据是否已经过期的另一个领域,其与TCS当前是否为空或不合并的知识,将是触发你是否真正走出去,再次加载数据。

好了,少废话,它可能会做很多更有意义,如果你看到它。

的code

 公共类的CustomerService
{
    //你的缓存时间(使用15分钟为例,可以从配置或其它地方加载)
    私人静态只读时间跨度CustomersCacheTimeout =新的时间跨度(0,15,0);

    //锁定对象,用于提供线程安全
    私有对象loadCustomersLock =新的对象();
    私人TaskCompletionSource< IEnumerable的<客户>> loadCustomersTaskCompletionSource;
    私营的DateTime loadCustomersLastCacheTime = DateTime.MinValue;

    私人任务< IEnumerable的<客户>> LoadCustomersAsync()
    {
        锁定(this.loadCustomersLock)
        {
            布尔needToLoadCustomers = this.loadCustomersTaskCompletionSource == NULL
                                             ||
                                       (this.loadCustomersTaskCompletionSource.Task.IsFaulted || this.loadCustomersTaskCompletionSource.Task.IsCanceled)
                                             ||
                                       DateTime.Now  -  this.loadCustomersLastCacheTime.Value> CustomersService.CustomersCacheTimeout;

            如果(needToLoadCustomers)
            {
                this.loadCustomersTaskCompletionSource =新TaskCompletionSource< IEnumerable的<客户>>();

                尝试
                {
                     // GetAsync返回任务< Htt的presponseMessage>
                     Client.GetAsync(URI).ContinueWith(前因=>
                     {
                        如果(antecedent.IsCanceled)
                        {
                            this.loadCustomersTaskCompletionSource.SetCanceled();
                        }
                        否则,如果(antecedent.IsFaulted)
                        {
                            this.loadCustomersTaskCompletionSource.SetException(antecedent.Exception);
                        }
                        其他
                        {
                            //转换的Htt presponseMessage到所需的返回类型
                            VAR响应= antecedent.Result;

                            VAR列表= response.Content.ReadAs< IEnumerable的<客户>>();

                            this.loadCustomersTaskCompletionSource.SetResult(名单);

                            //记录最后缓存时间
                            this.loadCustomersLastCacheTime = DateTime.Now;
                        }
                    });
                }
                赶上(例外前)
                {
                    this.loadCustomersTaskCompletionSource.SetException(前);
                }
            }
        }
    }

    返回this.loadCustomersTaskCompletionSource.Task;
}
 

场景将不会加载客户:

  1. 如果这是第一次调用,因此TCS将被创建并获取客户的TCS将为空。
  2. 如果在previous电话故障或者被取消,新的TCS将被创建和客户获取。
  3. 如果缓存超时已到期,新的TCS将被创建和客户获取。

方案,其中客户加载/加载:

  1. 如果该客户的在加载过程的,现有的TCS的任务将被退回的任何延续添加到任务使用 ContinueWith 将被执行,一旦TCS被通知。
  2. 如果该客户的已经加载的,现有的TCS的任务将被退回的任何延续添加到任务使用 ContinueWith 将被执行一旦调度程序认为合适的。

注意:我用晶粒粗大这里锁定的方法和理论上你就可以改善与读/写器实现的性能,但它可能会是一个微型的优化你的情况

(Note: this is an over-simplified scenario to demonstrate my coding issue.)

I have the following class interface:

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

This is the client-side of a RESTful API which loads a list of Customer objects from the server then exposes methods that allows client code to consume and work against that list.

Both of these methods work against the internal list of Customers retrieved from the server as follows:

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);
    }
}

The Client class is a custom version of the HttpClient class from the WCF Web API (now ASP.NET Web API) because I am working in Silverlight and they don't have an SL version of their client assemblies.

After all that background, here's my problem:

All of the methods in the CustomerService class use the list returned by the asynchronous LoadCustomersAsync method; therefore, any calls to these methods should wait (asynchronously) until the LoadCustomers method has returned and the appopriate logic executed on the returned list.

I also only want one call made from the client (in LoadCustomers) at a time. So, I need all of the calls to the public methods to wait on the same internal task.

To review, here's what I need to figure out how to accomplish:

  1. Any call to FindCustomersInArea and GetCustomerByName should return a Task that waits for the LoadCustomersAsync method to complete. If LoadCustomersAsync has already returned (and the cached list still valid), then the method may continue immediately.

  2. After LoadCustomersAsync returns, each method has additional logic required to convert the list into the desired return value for the method.

  3. There must only ever be one active call to LoadCustomersAsync (of the GetAsync method within).

  4. If the cached list expires, then subsequent calls will trigger a reload (via LoadCustomersAsync).

Let me know if you need further clarification, but I'm hoping this is a common enough use case that someone can help me work out the logic to get the client working as desired.

解决方案

Disclaimer: I'm going to assume you're using a singleton instance of your HttpClient subclass. If that's not the case we need only modify slightly what I'm about to tell you.


Yes, this is totally doable. The mechanism we're going to rely on for subsequent calls to LoadCustomersAsync is that if you attach a continuation to a Task, even if that Task completed eons ago, you're continuation will be signaled "immediately" with the task's final state.

Instead of creating/returning a new TaskCompletionSource<T> (TCS) every time from the LoadCustomerAsync method, you would instead have a field on the class that represents the TCS. This will allow your instance to remember the TCS that last represented the call that represented a cache-miss. This TCS's state will be signaled exactly the same as your existing code. You'll add the knowledge of whether or not the data has expired as another field which, combined with whether the TCS is currently null or not, will be the trigger for whether or not you actually go out and load the data again.

Ok, enough talk, it'll probably make a lot more sense if you see it.

The Code

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; 
}

Scenarios where the customers aren't loaded:

  1. If it's the first call, the TCS will be null so the TCS will be created and customers fetched.
  2. If the previous call faulted or was canceled, a new TCS will be created and the customers fetched.
  3. If the cache timeout has expired, a new TCS will be created and the customers fetched.

Scenarios where the customers are loading/loaded:

  1. If the customers are in the process of loading, the existing TCS's Task will be returned and any continuations added to the task using ContinueWith will be executed once the TCS has been signaled.
  2. If the customers are already loaded, the existing TCS's Task will be returned and any continuations added to the task using ContinueWith will be executed as soon as the scheduler sees fit.

NOTE: I used a coarse grained locking approach here and you could theoretically improve performance with a reader/writer implementation, but it would probably be a micro-optimization in your case.

这篇关于拥有多个呼叫等待相同的内部异步任务的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆