在同步函数中进行异步调用

10

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

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<T>,其中没有任何一种可转换为 "System.Func"。

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

更新:评论区中建议使用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())?


2
不是答案,但或许对你有趣:你看过AsyncLazy吗? - default
1
好的,你不能使用你现有的接口来完成它。await只在异步层之间起作用,而你缺少了一个 - GetOrAdd方法本身。你需要一个自己本身就是异步的版本,并等待整个GetOrAdd方法(注意,你现在实际上并没有这样做 - 这是另一个问题)。不幸的是,这意味着要编写自己的ConcurrentDictionary,这会很痛苦。 - Luaan
@UserControl 你的假设是正确的。async/await 的整个目标就是简化异步代码,它适用于异步方法(API),但不适用于非异步的API。你试图异步访问同步API,这实际上是没有意义的。好吧,如果你对我的答案还不满意,你可以在你的GetStuffAsync版本2上多做一些工作,使其线程安全,那应该就没问题了。 - Sriram Sakthivel
https://dev59.com/C2Ei5IYBdhLWcg3wHJER - UserControl
@UserControl:你如何使用AsyncLazy来解决你的问题?我无法弄清楚如何神奇地避免编译错误? - sonatique
显示剩余2条评论
1个回答

24

异步方法(或 lambda 表达式)只能返回 voidTask 或者 Task<T>,但是你的 lambda 返回的是 string[],因此编译器会阻止你进行操作。

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

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(...);

第一次它将等待缓存的任务完成--然后它将继续同步执行。

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


1
你可以直接写 x => LoadAsync(x),而不是写成 async (x) => await LoadAsync(x)。它们是同一个方法。 - Servy
2
实际上,你甚至可以写成 return data.GetOrAdd(key, LoadAsync); :) - UserControl
@UserControl 哦,你说得对。感谢委托的隐式转换 :) 我也更新了我的答案。 - Sriram Sakthivel
1
这种方法的一个缺点是,如果任务失败一次(可能是由于网络错误),ConcurrentDictionary条目将是一个每当等待它时都会抛出异常的任务。在这种情况下,您可能需要一些额外的逻辑来使缓存条目无效。 - StriplingWarrior
1
@StriplingWarrior 很好的观点,我也在我的答案中加入了这个备注。可以通过在返回任务(刚创建的任务)之前设置一个故障继续来处理它,然后从缓存中删除它。 - Sriram Sakthivel
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接