首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >缓存异步操作

缓存异步操作
EN

Stack Overflow用户
提问于 2014-02-07 11:01:21
回答 6查看 9.2K关注 0票数 19

我正在寻找一种优雅的方法来缓存异步操作的结果。

我最初有一个类似于这样的同步方法:

代码语言:javascript
复制
public String GetStuff(String url)
{
    WebRequest request = WebRequest.Create(url);
    using (var response = request.GetResponse())
    using (var sr = new StreamReader(response.GetResponseStream()))
        return sr.ReadToEnd();
}

然后我把它变成异步的:

代码语言:javascript
复制
public async Task<String> GetStuffAsync(String url)
{
    WebRequest request = WebRequest.Create(url);
    using (var response = await request.GetResponseAsync())
    using (var sr = new StreamReader(response.GetResponseStream()))
        return await sr.ReadToEndAsync();
}

然后,我决定缓存结果,因此我不需要经常在外部查询:

代码语言:javascript
复制
ConcurrentDictionary<String, String> _cache = new ConcurrentDictionary<String, String>();

public async Task<String> GetStuffAsync(String url)
{
    return _cache.GetOrAdd(url, await GetStuffInternalAsync(url));
}

private async Task<String> GetStuffInternalAsync(String url)
{
    WebRequest request = WebRequest.Create(url);
    using (var response = await request.GetResponseAsync())
    using (var sr = new StreamReader(response.GetResponseStream()))
        return await sr.ReadToEndAsync();
}

然后,我读了一篇关于缓存Task<T>如何更好的文章(o观看了一段视频),因为创建它们是很昂贵的:

代码语言:javascript
复制
ConcurrentDictionary<String, Task<String>> _cache = new ConcurrentDictionary<String, Task<String>>();

public Task<String> GetStuffAsync(String url)
{
    return _cache.GetOrAdd(url, GetStuffInternalAsync(url));
}

private async Task<String> GetStuffInternalAsync(String url)
{
    WebRequest request = WebRequest.Create(url);
    using (var response = await request.GetResponseAsync())
    using (var sr = new StreamReader(response.GetResponseStream()))
        return await sr.ReadToEndAsync();
}

现在的问题是,如果请求失败(例如:HTTP401),缓存将包含一个失败的Task<String>,而我将不得不重置应用程序,因为重新发送请求是不可能的。

是否有一种优雅的方法可以使用ConcurrentDictionary<T1,T2>缓存成功的任务,并且仍然具有原子行为?

EN

回答 6

Stack Overflow用户

回答已采纳

发布于 2014-02-07 12:26:49

首先,您的两种方法都是错误的,因为它们不会为您节省任何请求(尽管第二种方法至少可以节省您的时间)。

您的第一个代码(带有await的代码)执行以下操作:

  1. 提出请求。
  2. 等待请求完成。
  3. 如果缓存中已有结果,则忽略请求的结果。

第二段代码删除了步骤2,因此速度更快,但仍然会发出大量不必要的请求。

相反,您应该使用这需要一名代表

代码语言:javascript
复制
public Task<String> GetStuffAsync(String url)
{
    return _cache.GetOrAdd(url, GetStuffInternalAsync);
}

这并不能完全消除被忽略的请求的可能性,但它确实大大降低了这些请求的可能性。(为此,您可以尝试取消您知道正在被忽略的请求,但我认为这不值得我们为此付出努力。)

现在谈谈你的实际问题。我认为你应该做的是使用方法。如果值还没有存在,那么添加它。如果它在那里,如果它有故障,就更换它:

代码语言:javascript
复制
public Task<String> GetStuffAsync(String url)
{
    return _cache.AddOrUpdate(
        url, GetStuffInternalAsync, (u, task) =>
        {
            if (task.IsCanceled || task.IsFaulted)
                return GetStuffInternalAsync(u);
            return task;
        });
}
票数 18
EN

Stack Overflow用户

发布于 2014-02-07 12:44:21

将这些失败的任务保留为负缓存实际上是合理的(并且取决于您的设计和性能,这是至关重要的)。否则,如果一个url总是失败,那么一次又一次地使用它就完全失败了。

您需要的是一种不时清除缓存的方法。最简单的方法是拥有一个替换ConcurrentDictionarry实例的计时器。更健壮的解决方案是构建自己的LruDictionary或类似的东西。

票数 7
EN

Stack Overflow用户

发布于 2016-01-17 03:33:15

另一种简单的方法是将Lazy<T>扩展为AsyncLazy<T>,如下所示:

代码语言:javascript
复制
public class AsyncLazy<T> : Lazy<Task<T>>
{
    public AsyncLazy(Func<Task<T>> taskFactory, LazyThreadSafetyMode mode) :
        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap(), mode)
    { }

    public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
}

然后你就可以这样做:

代码语言:javascript
复制
private readonly ConcurrentDictionary<string, AsyncLazy<string>> _cache
    = new ConcurrentDictionary<string, AsyncLazy<string>>();

public async Task<string> GetStuffAsync(string url)
{
    return await _cache.GetOrAdd(url,
        new AsyncLazy<string>(
            () => GetStuffInternalAsync(url),
            LazyThreadSafetyMode.ExecutionAndPublication));
}
票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/21626014

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档