NET возобновляет ожидание продолжения в новом потоке другого пула потоков или повторно использует поток из предыдущего возобновления? - PullRequest
2 голосов
/ 08 января 2020

. NET возобновить ожидание продолжения в новом потоке другого пула потоков или повторно использовать поток из предыдущего возобновления?

Давайте представим код ниже C# в приложении консоли *. 1036 * Core. :

using System;
using System.Threading;
using System.Threading.Tasks;

namespace NetCoreResume
{
    class Program
    {
        static async Task AsyncThree()
        {
            await Task.Run(() =>
            {
                Console.WriteLine($"AsyncThree Task.Run thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
            });

            Console.WriteLine($"AsyncThree continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
        }

        static async Task AsyncTwo()
        {
            await AsyncThree();

            Console.WriteLine($"AsyncTwo continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
        }

        static async Task AsyncOne()
        {
            await AsyncTwo();

            Console.WriteLine($"AsyncOne continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
        }

        static void Main(string[] args)
        {
            AsyncOne().Wait();

            Console.WriteLine("Press any key to end...");
            Console.ReadKey();
        }
    }
}

Будет выведено:

AsyncThree Task.Run thread id:4
AsyncThree continuation thread id:4
AsyncTwo continuation thread id:4
AsyncOne continuation thread id:4
Press any key to end...

Я пытался добавить ConfigureAwait(false) после каждого ожидания Task, но получит тот же результат.

Как мы видим, все ожидающие продолжения повторно используют поток, созданный в методе Task.Run из AsyncThree(). Я хочу спросить, будет ли. NET всегда возобновлять ожидание продолжения в предыдущем потоке возобновления, или в некоторых случаях будет применяться новый поток из пула потоков?

Я знал, что ответ есть, продолжение продолжится поток пула потоков в обсуждении ниже:

async / await. Где выполняется продолжение ожидаемой части метода?

Давайте исключим случай SynchronizationContext в приведенной выше ссылке, поскольку мы сейчас обсуждаем консольное приложение. NET. Но я хочу спросить, кажется, , что поток пула потоков в моем примере всегда thread id 4, я не знаю, так ли это, потому что thread id 4 всегда свободен в пуле потоков, поэтому каждое продолжение использовать его по стечению обстоятельств, или. NET имеет ли механизм максимально возможное повторное использование предыдущего потока возобновления?

Есть ли возможность возобновления каждого продолжения в другом потоке пула потоков, как показано ниже?

AsyncThree Task.Run thread id:4
AsyncThree continuation thread id:5
AsyncTwo continuation thread id:6
AsyncOne continuation thread id:7
Press any key to end...

Ответы [ 4 ]

3 голосов
/ 11 января 2020

. NET возобновить ожидание продолжения в новом потоке другого пула потоков или повторно использовать поток из предыдущего возобновления?

Ни того, ни другого. По умолчанию, когда await ing Task s, await захватывает «контекст» и использует его для возобновления асинхронного метода . Этот «контекст» равен SynchronizationContext.Current, если он не равен null, и в этом случае контекст равен TaskScheduler.Current. В вашем примере кода контекст является контекстом пула потоков.

Другая часть головоломки недокументирована: await использует флаг TaskContinuationOptions.ExecuteSynchronously . Это означает, что когда задача Task.Run завершена (потоком 4), ее продолжения запускаются немедленно и синхронно - , если возможно . В вашем примере кода продолжение может выполняться синхронно, потому что в потоке 4 достаточно стека, а продолжение должно выполняться в потоке пула потоков, а поток 4 является потоком пула потоков.

Аналогично, когда AsyncThree завершается, продолжение для AsyncTwo запускается немедленно и синхронно - снова в потоке 4, поскольку оно соответствует всем критериям.

Эта оптимизация особенно полезна в сценарии ios, как ASP. NET, где принято иметь цепочку из async методов и завершить одну задачу (например, чтение базы данных), которая завершает всю цепочку и отправляет ответ. В этих случаях вы хотите избежать ненужных переключателей потоков.

Интересным побочным эффектом этого является то, что вы в итоге получаете своего рода «инвертированный стек вызовов»: поток пула потоков 4 запустил ваш код, а затем завершено AsyncThree, затем AsyncTwo, а затем AsyncOne, и каждое из этих завершений находится в фактическом стеке вызовов. Если вы установите точку останова на WriteLine в AsyncOne (и посмотрите на внешний код), вы увидите, где ThreadPoolWorkQueue.Dispatch (косвенно) вызывается AsyncThree, который (косвенно) вызывает AsyncTwo, который (косвенно) вызывается AsyncOne.

1 голос
/ 08 января 2020

. NET возобновить ожидание продолжения в новом потоке другого пула потоков или повторно использовать поток из предыдущего возобновления?

Большую часть времени он будет использовать тот же поток , но это не гарантировано. Этот ответ на вопрос, в котором ОП хочет принудительно передать его в тот же поток, дает некоторые подробности.

Если выполняется продолжение, то до TaskScheduler . Я не смотрел, но я предполагаю, что он будет запускать продолжение в том же потоке, чтобы избежать ненужных накладных расходов при работе с пулом потоков.

Есть ли вероятность, что каждое продолжение будет возобновлено в другом потоке? поток пула, как показано ниже?

Да, есть возможность, но вероятно, что вы не увидите этого снова из-за планировщика. Возможно, вам придется написать свой собственный, чтобы вызвать разницу, и лично я не знаю, почему вы это сделали. (Не говоря, что это было твоим намерением).

1 голос
/ 08 января 2020

Как вы можете видеть Почему начальный поток не используется в коде после ожидаемого метода? вполне возможно возобновить работу в другом потоке, основываясь на том, что доступно в данный момент.

В асинхронном программировании нет определенного использования определенных c потоков при использовании с asyn c await. Вы только знаете, что из пула потоков будет выбран доступный поток.

В вашем случае, поскольку выполнение в значительной степени последовательное, поток освобождается, и вы получаете число 4.

На основании документации пула потоков https://docs.microsoft.com/en-us/dotnet/standard/threading/the-managed-thread-pool пул потоков уникален для каждого процесса, поэтому я ожидаю, что будет использоваться первый доступный поток. Так как у вас нет других параллельных операций, поток 4 будет повторно использоваться каждый раз. Хотя нет никаких гарантий.

0 голосов
/ 08 января 2020

В вашем примере кода ie нет действительного асинхронного вызова c. Вызов ввода / вывода, поэтому скорее всего продолжение встроено в исходный поток в качестве оптимизации. В ваших asyn c методах, если вы добавите await Task.Delay, вы можете заметить, что продолжение может выполняться в другом потоке. Нижняя линия никогда не делает никаких предположений, по какому потоку работают продолжения, предположим, что он запускается в другом потоке.

...