简体   繁体   English

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

[英]How to execute nested async/await code in parallel while maintaining the same thread on await continuations?

This might be the worst StackOverflow title I've ever written. 这可能是我写过的最糟糕的StackOverflow标题。 What I'm actually trying to do is execute an asynchronous method that uses the async/await convention (and itself contains additional await calls) from within a synchronous method multiple times in parallel while maintaining the same thread throughout the execution of each branch of the parallel execution, including for all await continuations. 我实际上要做的是执行一个异步方法,该方法在同步方法中多次并行使用async / await约定(并且本身包含额外的await调用),同时在整个执行每个分支的过程中保持相同的线程。并行执行,包括所有等待延续。 To put it another way, I want to execute some async code synchronously, but I want to do it multiple times in parallel. 换句话说,我想同步执行一些异步代码,但我想多次并行执行。 Now you can see why the title was so bad. 现在你可以看到为什么标题太糟糕了。 Perhaps this is best illustrated with some code... 也许最好用一些代码来说明......

Assume I have the following: 假设我有以下内容:

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...
    }
}

The caller is synchronous (from a console application). 调用者是同步的(来自控制台应用程序)。 Let me try illustrating what I'm trying to do with an attempt to use Task.WaitAll(...) and wrapper tasks: 让我尝试说明我尝试使用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());
}

The desired behavior is for MethodA , MethodB , and MethodC to all be run on the same thread, both before and after the continuation, and for this to happen 4 times in parallel on 4 different threads. MethodAMethodBMethodC所需的行为是在连续之前和之后都在同一个线程上运行,并且在4个不同的线程上并行发生4次。 To put it yet another way, I want to remove the asynchronous behavior of my await calls since I'm making the calls parallel from the caller. 换句话说,我想删除我的await调用的异步行为,因为我正在从调用者并行调用。

Now, before I go any further, I do understand that there's a difference between asynchronous code and parallel/multi-threaded code and that the former doesn't imply or suggest the latter. 现在,在我进一步讨论之前,我确实理解异步代码和并行/多线程代码之间存在差异,前者并不暗示或暗示后者。 I'm also aware the easiest way to achieve this behavior is to remove the async/await declarations. 我也知道实现此行为的最简单方法是删除async / await声明。 Unfortunately, I don't have the option to do this (it's in a library) and there are reasons why I need the continuations to all be on the same thread (having to do with poor design of said library). 不幸的是,我没有选择这样做(它在库中)并且有理由为什么我需要延续所有在同一个线程上(与所述库的不良设计有关)。 But even more than that, this has piqued my interest and now I want to know from an academic perspective. 但更重要的是,这引起了我的兴趣,现在我想从学术角度来了解。

I've attempted to run this using PLINQ and immediate task execution with .AsParallel().Select(x => x.MethodA().Result) . 我试图使用PLINQ和使用.AsParallel().Select(x => x.MethodA().Result)立即执行任务来运行它.AsParallel().Select(x => x.MethodA().Result) I've also attempted to use the AsyncHelper class found here and there, which really just uses .Unwrap().GetAwaiter().GetResult() . 我也试图使用在这里和那里找到的AsyncHelper类,它实际上只使用.Unwrap().GetAwaiter().GetResult() I've also tried some other stuff and I can't seem to get the desired behavior. 我也试过其他一些东西,我似乎无法得到理想的行为。 I either end up with all the calls on the same thread (which obviously isn't parallel) or end up with the continuations executing on different threads. 我要么最终得到同一个线程上的所有调用(显然不是并行),要么以不同线程上执行的连续结束。

Is what I'm trying to do even possible, or are async/await and the TPL just too different (despite both being based on Task s)? 是我正在尝试做甚至可能,或者是async / await和TPL太不同了(尽管两者都基于Task )?

The methods that you are calling do not use ConfigureAwait(false) . 您调用的方法不使用ConfigureAwait(false) This means that we can force the continuations to resume in a context we like. 这意味着我们可以强制继续在我们喜欢的上下文中恢复。 Options: 选项:

  1. Install a single-threaded synchronization context. 安装单线程同步上下文。 I believe Nito.Async has that. 我相信Nito.Async有这个。
  2. Use a custom TaskScheduler . 使用自定义TaskScheduler await looks at TaskScheduler.Current and resumes at that scheduler if it is non-default. await查看TaskScheduler.Current并在该调度程序中恢复,如果它是非默认的。

I'm not sure if there are any pros and cons for either option. 我不确定这两种选择是否有任何利弊。 Option 2 has easier scoping I think. 选项2我认为更容易确定范围。 Option 2 would look like: 选项2看起来像:

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

Call this once for each parallel invocation and use Task.WaitAll to join all those tasks. 为每个并行调用调用一次,并使用Task.WaitAll加入所有这些任务。 Probably you should dispose of that scheduler as well. 也许你应该处理那个调度程序。

I'm (ab)using ConcurrentExclusiveSchedulerPair here to get a single-threaded scheduler. 我(ab)在这里使用ConcurrentExclusiveSchedulerPair来获取单线程调度程序。

If those methods are not particularly CPU-intensive you can just use the same scheduler/thread for all of them. 如果这些方法不是特别占用CPU,那么可以对所有这些方法使用相同的调度程序/线程。

You can create 4 independent threads, each one executes MethodA with a limited-concurrency (actually, no concurrency at all) TaskScheduler. 您可以创建4个独立的线程,每个线程使用有限并发(实际上根本没有并发)TaskScheduler执行MethodA。 That will ensure that every Task, and continuation Tasks, that the thread creates, will be executed by that thread. 这将确保线程创建的每个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...
    }

That won't ensure you a 4th degree parallelism, of course. 当然,这并不能确保你获得4度并行性。

You can see an implementation of such TaskScheduler in MSDN - https://msdn.microsoft.com/en-us/library/ee789351(v=vs.110).aspx 你可以在MSDN中看到这样的TaskScheduler的实现 - https://msdn.microsoft.com/en-us/library/ee789351(v=vs.110).aspx

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM