WhenAll 是顺序执行还是并发执行?

3
每次运行下面的代码时,Mike的异常都会被捕获。
WhenAll是顺序执行的吗?在每个任务之间有一个延续上下文吗?还是所有任务都并发运行?如果是并发的,为什么总是捕获Mike的异常而不是Mitch的异常?我在Mike上设置了延迟,以给Mitch一个机会。如果是顺序执行,如何使其并发?在进行Web请求/文件处理时是否应用并发执行?
假设这段代码更为严肃,是否采用异步方式是明智的做法?场景是多个方法Jason、Mitch和Mike并发运行,不会阻塞,并在所有完成后继续事件处理程序。关于我天真的异常处理实现,应该注意哪些考虑因素?有哪些问题或潜在问题需要注意?
private async void button1_Click(object sender,EventArgs e)
{
    try
    {
        AsyncJason c1 = new AsyncJason();
        await c1.Hello();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

public class AsyncJason
{
    public AsyncJason()
    {
    }

    public async Task Hello()
    {
        var j = await GetJasonAsync();
        string[] dankeSchon = await Task.WhenAll(new Task<string>[] {GetJasonAsync(), GetMikeAsync(), GetMitchAsync()});
    }

    private async Task<string> GetJasonAsync()
    {
        var result = await Task.Run<string>(() => GetJason());
        return result;
    }

    private string GetJason()
    {
        return "Jason";
    }

     private async Task<string> GetMitchAsync()
    {
        var result = await Task.Run<string>(() => GetMitch());
        return result;
    }

    private string GetMitch()
    {
        throw new ArgumentException("Mitch is an idiot", "none");
    }

     private async Task<string> GetMikeAsync()
    {
        await Task.Delay(3000);
        var result = await Task.Run<string>(() => GetMike());
        return result;
    }

    private string GetMike()
    {
        throw new ArgumentException("Mike is an idiot", "none");
    }
}

2
WhenAll 无法控制任务的执行方式(即所有任务都可以在执行 WhenAll 时完成)... - Alexei Levenkov
1个回答

8

WhenAll是顺序执行还是并发执行的?

这个问题其实不适用。当所有底层任务完成时,WhenAll的任务就完成了。如何实现这一点是它自己的事情。

当涉及到异常时,TaskException属性包含一个AggregateException,其中包含所有底层任务抛出的异常。

当您等待代表多个异常的聚合异常的任务时,它将解开并重新抛出该列表中的第一个异常,而不是带有所有异常的AggregateException

在创建AggregateException时,它(显然;我不知道这是否在任何地方都得到保证)根据传递给WhenAll的任务的顺序而不是根据这些任务完成的顺序列出异常。

如果您担心丢失异常,则应存储它返回的任务,以便您可以检查所有异常,或者只需重新抛出包装的AggregateException,即:

public async Task Hello()
{
    var j = await GetJasonAsync();
    var task = Task.WhenAll(new Task<string>[] { GetJasonAsync(), GetMikeAsync(), GetMitchAsync() });
    try
    {
        string[] dankeSchon = await task;
    }
    catch (Exception)
    {
        throw task.Exception;
    }
}

如果你真的想让第一个触发异常的任务成为重新抛出的任务,那是可以做到的。一种选择是基本上重写WhenAll成为我们自己的版本,只是稍微不同地处理异常。另一种选择是根据它们完成的顺序对任务进行排序,即使不知道任务的任何信息也能保持异步性。这里有一个Order方法,它接受一系列任务并返回表示相同操作的任务序列,但根据完成时间(按升序)排序。
public static IEnumerable<Task<T>> Order<T>(this IEnumerable<Task<T>> tasks)
{
    var taskList = tasks.ToList();

    var taskSources = new BlockingCollection<TaskCompletionSource<T>>();

    var taskSourceList = new List<TaskCompletionSource<T>>(taskList.Count);
    foreach (var task in taskList)
    {
        var newSource = new TaskCompletionSource<T>();
        taskSources.Add(newSource);
        taskSourceList.Add(newSource);

        task.ContinueWith(t =>
        {
            var source = taskSources.Take();

            if (t.IsCanceled)
                source.TrySetCanceled();
            else if (t.IsFaulted)
                source.TrySetException(t.Exception.InnerExceptions);
            else if (t.IsCompleted)
                source.TrySetResult(t.Result);
        }, CancellationToken.None, TaskContinuationOptions.PreferFairness, TaskScheduler.Default);
    }

    return taskSourceList.Select(tcs => tcs.Task);
}

本质上的想法是为每个任务创建一个 TaskCompletionSource,为我们提供的每个任务添加 continuation,并且当任何任务完成时,将尚未完成的 TaskCompletionSource 标记为刚完成的任务的结果。

使用这个方法,我们现在可以编写:

public async Task Hello()
{
    var j = await GetJasonAsync();
    var tasks = new[] { GetJasonAsync(), GetMikeAsync(), GetMitchAsync() };
    string[] dankeSchon = await Task.WhenAll(tasks.Order());
}

异常将是首先抛出的异常。


“throw task.Exception;” 不会清除异常的堆栈跟踪吗? - Scott Chamberlain
@ScottChamberlain,关于聚合异常,是的。至于内部异常,我不会这么想。我记得看到过一个C# 5.0工具的示例,可以在不清除堆栈的情况下重新抛出异常,但我没有花时间再去找它;理论上可以在这里应用它。 - Servy

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