【问题标题】:How to access a variable that was available when an async method was called?如何访问调用异步方法时可用的变量?
【发布时间】:2021-11-21 18:17:36
【问题描述】:

动物名称是从 API 中获取的,如果找不到动物,该 API 可能会返回 404。但为了正确记录错误,我们需要访问动物所在的国家/地区。可能吗?我读过something from a guy called Stephen Cleary,这让我觉得 lambdas 是可能的,但我找不到任何东西。

var gettingNames = new List<Task<string>>();

foreach (var animal in animals)
{
    gettingNames.Add(this.zooApi.GetNameAsync(animal));
}

try
{
    await Task.WhenAll(gettingNames);
}
catch (Exception e)
{
    var exception = gettingNames.Where(task => task.IsFaulted)
        .SelectMany(x => x.Exception.InnerExceptions).First();

    this.logger.LogError("The animal name from {Country} was not found",
        animal.Country); // This is the goal
}

【问题讨论】:

  • 您需要一种方法将错误任务转换回引发其创建的动物。您可能希望创建一个从任务对象到动物的字典映射,以用于您的异常处理。
  • 你的意思是@StephenCleary? stackoverflow.com/users/263693/stephen-cleary :)

标签: c# exception async-await task-parallel-library


【解决方案1】:

解决此问题的一种方法是将每个Animal 投影到Task,其中包含比裸名称或裸错误更多的信息。例如,您可以将其投影到包含三条信息的 Task&lt;ValueTuple&lt;Animal, string, Exception&gt;&gt;:动物、zooApi 中的动物学名,以及调用 zooApi.GetScientificNameAsync 方法时可能发生的错误。

执行此投影的最简单方法是 LINQ Select 运算符:

List<Task<(Animal, string, Exception)>> tasks = animals.Select(async animal =>
{
    try
    {
        return (animal, await this.zooApi.GetScientificNameAsync(animal),
            (Exception)null);
    }
    catch (Exception ex)
    {
        return (animal, null, ex);
    }
}).ToList();

(Animal, string, Exception)[] results = await Task.WhenAll(tasks);

foreach (var (animal, scientificName, error) in results)
{
    if (error != null)
        this.logger.LogError(error,
            $"The {animal.Name} from {animal.Country} was not found");
}

【讨论】:

  • 我喜欢这个想法,但是,这段代码不是在连续等待每个 GetScientificNameAsync,而不是同时执行所有操作吗?我的意思是,这似乎在等待每次评估返回时,然后,当 WhenAll 命中时,所有任务都已解决。
  • @user33276346 乍一看,GetScientificNameAsync 似乎是为每个动物连续调用和等待的,但实际上它是为所有动物同时调用的。 Select 运算符的选择器内的await 不会等待投影的Task&lt;(Animal, string, Exception)&gt;。它是生成此任务的异步 lambda 中的 await。如果您是第一次看到这种模式,可能很难理解它。这是被称为“async-await 组合”的强大技术的一个例子。
  • 这里的诀窍是.ToList(),它会导致.Select(...) lambda 为每个元素执行,并在它们被添加到列表时启动每个任务。
  • @TheodorZoulias 根据您的回答,我想出了一个更简洁的替代方案,但我不确定它是否会像您的一样工作,您觉得如何? stackoverflow.com/a/69392220
  • @JeremyLakeman 不会在 whenall 调用中调用它吗?
【解决方案2】:

你几乎成功了。 :)

您需要Dictionary&lt;Task&lt;string&gt;, string&gt; 结构而不是List&lt;Task&lt;string&gt;&gt;

static async Task Main()
{
    var taskInputMapping = new Dictionary<Task<string>, string>();
    var inputs = new[] { "input", "fault", "error", "test"};
    foreach (var input in inputs)
    {
        taskInputMapping.Add(DelayEcho(input), input);
    }

    try
    {
        await Task.WhenAll(taskInputMapping.Keys);
    }
    catch
    {
        foreach (var pair in taskInputMapping.Where(t => t.Key.IsFaulted))
        {
            Console.WriteLine($"{pair.Value}: {pair.Key.Exception?.GetType().Name}");
        }
    }
}

static readonly ImmutableArray<string> wrongInputs = 
    ImmutableArray.Create("error", "fault");
static async Task<string> DelayEcho(string input)
{
    if (wrongInputs.Contains(input)) throw new ArgumentException();
    await Task.Delay(10);
    return input;
}
  • taskInputMapping.Add(DelayEcho(input), input):将输入存储在任务本身旁边
  • taskInputMapping.Where(t =&gt; t.Key.IsFaulted): 遍历出错的任务
  • $"{pair.Value}: {pair.Key.Exception?.GetType().Name}":检索输入+相关错误

【讨论】:

  • 我想出了一个替代方案,但我不确定它是否能与您的同时工作,您觉得如何? stackoverflow.com/a/69392220
  • @user33276346 您的解决方案更加灵活。如果GetNameAsync 不仅需要一个参数,而不是2 个或更多参数,那么您不需要修改太多。我的方法需要进一步调整以允许这样做。
  • @user33276346 另一方面,您的解决方案在原始请求周围引入了一个包装器方法,以使其故障安全。异常堆栈跟踪可能看起来有点奇怪,因为您的包装器位于 Select 内。在我的情况下,堆栈跟踪与按顺序调用时保持相同。
【解决方案3】:

我结合了答案并想出了这个:

var tasks = animals.Select(async animal =>
{
    try
    {
        return await this.zooApi.GetNameAsync(animal);
    }
    catch (Exception ex)
    {
        this.logger.LogError(error,
            $"The {animal.Name} from {animal.Country} was not found");

        return null;
    }
});

var results = await Task.WhenAll(tasks);

foreach (var name in results.Where(x => x != null))...

【讨论】:

  • 没关系。与我的回答不同的是,在您的解决方案中,错误一经发生就会同时记录,而在我的回答中,错误会在完成所有任务后在单个线程上集体记录。因此,您的解决方案要求 this.logger 实例是线程安全的。
猜你喜欢
  • 1970-01-01
  • 2017-03-06
  • 2017-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-18
  • 1970-01-01
相关资源
最近更新 更多