【问题标题】:Async call within synchronous function同步函数中的异步调用
【发布时间】:2015-03-19 22:44:53
【问题描述】:

我正在尝试异步填充我的缓存

static ConcurrentDictionary<string, string[]> data = new ConcurrentDictionary<string, string[]>();

public static async Task<string[]> GetStuffAsync(string key)
{
    return data.GetOrAdd(key, async (x) => {
        return await LoadAsync(x);
    });
}

static async Task<string[]> LoadAsync(string key) {....}

但这给了我错误:

无法将异步 lambda 表达式转换为委托类型“System.Func”。

异步 ​​lambda 表达式可能返回 void、Task 或 Task,它们都不能转换为 'System.Func'。

据我了解,这是因为 GetOrAdd() 不是异步的。我该如何解决这个问题?

更新: cmets 中建议的LazyAsync 将适用于我的简单示例。或者,像这样的解决方法(肯定可以忍受它引入的一些开销):

public static async Task<string[]> GetStuffAsync(string key)
{
    string[] d = null;
    if (!data.ContainsKey(key))
        d = await LoadAsync(key);
    return data.GetOrAdd(key, d);
}

那么问题就变成了微软只是没有时间更新所有接口以支持异步还是我试图做一些严重错误的事情(ConcurrentDictionary 不应该有GetOrAddAsync())?

【问题讨论】:

  • 不是答案,但可能对你来说很有趣:你见过AsyncLazy吗?
  • 嗯,你不能用你有的界面来做。 await 仅在跨异步层时才有效,而您缺少一个 - 方法 GetOrAdd 本身。您需要一个本身是异步的版本,并等待整个 GetOrAdd 方法(注意您现在实际上没有这样做 - 这是另一个问题)。可悲的是,这意味着要编写自己的 ConcurrentDictionary,这会造成伤害。
  • 为什么ConcurrentDictionary 应该有GetOrAddAsync ?它是内存中的同步收集。它没有理由有这样的方法。
  • @Sriram Sakthivel,那我肯定做错了,我想了解究竟是什么(可能我需要使用其他缓存模式或其他东西)。 async/await 的全部目标是简单的异步代码,对吧?尽管您的解决方案非常聪明,但我无法证明它的复杂性。
  • @UserControl 您的假设是正确的。 async/await 的全部目标是简单的异步代码,它适用于异步方法 (API),但不适用于非异步 API。您正在尝试异步访问同步 API;这实际上没有任何意义。好的,如果您不相信我的回答,您可以在 GetStuffAsync version2 上做一些工作以使其线程安全,这应该没问题。

标签: c# .net asynchronous async-await


【解决方案1】:

异步方法(或 lambda)只能返回 voidTaskTask&lt;T&gt; 但您的 lambda 返回 string[],因此编译器会阻止您。

await 关键字被优化为在任务已经完成时同步继续。因此,一种选择是将任务本身存储在字典中,而不必担心一次又一次地等待完成的任务。

private static ConcurrentDictionary<string, Task<string[]>> data =
    new ConcurrentDictionary<string, Task<string[]>>();

public static Task<string[]> GetStuffAsync(string key)
{
    return data.GetOrAdd(key, LoadAsync);
}

当你这样做时

var item = await GetStuffAsync(...);

第一次它会(a)等到缓存的任务完成——之后它将同步继续。

您必须考虑当LoadAsync 失败时会发生什么。因为我们正在缓存LoadAsync 返回的任务;如果失败,我们将愚蠢地缓存失败的任务。您可能需要处理这个问题。

【讨论】:

  • 实际上可以使用这种方法来围绕ConcurrentDictionary 构建一个包装器,它将公开异步GetOrAddAsync 方法并在其内部处理故障。手动锁定仍然会是很多棘手的工作,因此使用旧的同步模式之一来同步访问可能仍然是一个更好的主意。
  • 而不是写async (x) =&gt; await LoadAsync(x),你可以写x =&gt; LoadAsync(x)。它们是相同的方法。
  • 其实你甚至可以写return data.GetOrAdd(key, LoadAsync); :)
  • 这种方法的一个缺点是如果任务失败一次(可能是由于网络错误),ConcurrentDictionary 条目将是一个在等待时抛出异常的任务。发生这种情况时,您可能需要一些额外的逻辑来使缓存条目无效。
  • @StriplingWarrior 好点我还在我的回答中添加了这一点作为备注。这可以通过在返回任务(刚刚创建的任务)之前设置故障的延续来处理,然后将其从缓存中删除。
猜你喜欢
  • 2012-07-25
  • 2013-07-12
  • 2020-10-13
  • 2012-02-25
  • 2020-04-20
  • 1970-01-01
  • 2020-12-20
  • 2019-09-02
  • 1970-01-01
相关资源
最近更新 更多