如何在等待延续时保持相同的线程并行执行嵌套的async / await代码?

时间:2015-08-12 20:00:01

标签: c# multithreading asynchronous parallel-processing async-await

这可能是我写过的最糟糕的StackOverflow标题。我实际上要做的是执行一个异步方法,该方法在同步方法中多次并行使用async / await约定(并且本身包含额外的await调用),同时在整个执行每个分支的过程中保持相同的线程。并行执行,包括所有等待延续。换句话说,我想同步执行一些异步代码,但我想多次并行执行。现在你可以看到为什么标题太糟糕了。也许最好用一些代码来说明......

假设我有以下内容:

public class MyAsyncCode
{
    async Task MethodA()
    {
        // Do some stuff...
        await MethodB();
        // Some other stuff
    }

    async Task MethodB()
    {
        // Do some stuff...
        await MethodC();
        // Some other stuff
    }

    async Task MethodC()
    {
        // Do some stuff...
    }
}

调用者是同步的(来自控制台应用程序)。让我尝试说明我尝试使用Task.WaitAll(...)和包装器任务时要做的事情:

public void MyCallingMethod()
{
    List<Task> tasks = new List<Task>();
    for(int c = 0 ; c < 4 ; c++)
    {
        MyAsyncCode asyncCode = new MyAsyncCode();
        tasks.Add(Task.Run(() => asyncCode.MethodA()));
    }
    Task.WaitAll(tasks.ToArray());
}

所需的行为是MethodAMethodBMethodC都在同一个线程上运行,无论是在继续之前还是之后,并且这种行为发生了4次在4个不同的线程上并行。换句话说,我想删除await调用的异步行为,因为我正在从调用者那里调用并行调用。

现在,在我进一步讨论之前,我确实理解异步代码和并行/多线程代码之间存在差异,前者并不暗示或暗示后者。我也知道实现此行为的最简单方法是删除async / await声明。不幸的是,我没有选择这样做(它在库中)并且有原因为什么我需要延续所有在同一个线程上(与所述的设计不良有关)图书馆)。但更重要的是,这引起了我的兴趣,现在我想从学术角度来了解。

我试图使用PLINQ和.AsParallel().Select(x => x.MethodA().Result)立即执行任务来运行它。我还尝试使用此处和那里找到的AsyncHelper类,它实际上只使用.Unwrap().GetAwaiter().GetResult()。我也试过其他一些东西,我似乎无法得到理想的行为。我要么最终得到同一个线程上的所有调用(显然不是并行),要么以不同线程上执行的延续结束。

我正在尝试做什么,或者是async / await和TPL太不同了(尽管两者都基于Task s)?

2 个答案:

答案 0 :(得分:4)

您呼叫的方法不使用ConfigureAwait(false)。这意味着我们可以强制继续在我们喜欢的上下文中恢复。选项:

  1. 安装单线程同步上下文。我相信Nito.Async有这个。
  2. 使用自定义TaskSchedulerawait查看TaskScheduler.Current并在该调度程序中恢复,如果它是非默认的。
  3. 我不确定这两种选择是否有任何利弊。选项2我认为更容易确定范围。选项2看起来像:

    Task.Factory.StartNew(
        () => MethodA()
        , new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler).Unwrap();
    

    为每个并行调用调用一次,并使用Task.WaitAll加入所有这些任务。也许你应该处理那个调度程序。

    我(ab)在这里使用ConcurrentExclusiveSchedulerPair获取单线程调度程序。

    如果这些方法不是特别占用CPU,那么你可以为所有这些方法使用相同的调度程序/线程。

答案 1 :(得分:2)

您可以创建4个独立的线程,每个线程使用有限并发执行MethodA(实际上,根本没有并发)TaskScheduler。这将确保线程创建的每个Task和continuation Tasks都将由该线程执行。

    public void MyCallingMethod()
    {
        CancellationToken csl = new CancellationToken();
        var threads = Enumerable.Range(0, 4).Select(p =>
            {
                var t = new Thread(_ =>
                    {
                        Task.Factory.StartNew(() => MethodA(), csl, TaskCreationOptions.None,
                            new LimitedConcurrencyLevelTaskScheduler(1)).Wait();
                    });
                t.Start();  
                return t;
            }).ToArray();
        //You can block the main thread and wait for the other threads here...
    }

当然,这不能确保你获得4度并行性。

您可以在MSDN中看到此类TaskScheduler的实现 - https://msdn.microsoft.com/en-us/library/ee789351(v=vs.110).aspx