C#, зачем просто ждать Task.Delay (1); "разрешает" параллельное выполнение? - PullRequest
0 голосов
/ 10 июля 2020

Я хотел бы задать простой вопрос о приведенном ниже коде:

static void Main(string[] args)
        {
            MainAsync()
                //.Wait();
                .GetAwaiter().GetResult();
        }

        static async Task MainAsync()
        {
            Console.WriteLine("Hello World!");

            Task<int> a = Calc(18000);
            Task<int> b = Calc(18000);
            Task<int> c = Calc(18000);

            await a;
            await b;
            await c;

            Console.WriteLine(a.Result);
        }

        static async Task<int> Calc(int a)
        {
            //await Task.Delay(1);
            Console.WriteLine("Calc started");

            int result = 0;
           
            for (int k = 0; k < a; ++k)
            {
                for (int l = 0; l < a; ++l)
                {
                    result += l;
                }
            }

            return result;
        }

В этом примере функции Cal c выполняются синхронно. Когда строка «// ждем Task.Delay (1);» будут раскомментированы, функции Cal c будут выполняться параллельно.

Возникает вопрос: почему при добавлении простого await функция Cal c становится asyn c? Я знаю о требованиях к паре async / await. Я спрашиваю, что на самом деле происходит, когда в начале функции добавляется простая задержка ожидания. Вся функция Cal c затем распознается как запущенная в другом потоке, но почему?

Edit 1:

Когда я добавил проверку потока в код:

static async Task<int> Calc(int a)
        {
            await Task.Delay(1);
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

            int result = 0;
           
            for (int k = 0; k < a; ++k)
            {
                for (int l = 0; l < a; ++l)
                {
                    result += l;
                }
            }

            return result;
        }

можно увидеть (в консоли) разные идентификаторы потоков. Если удалить строку «await Delay», идентификатор потока всегда будет одинаковым для всех запусков функции Cal c. На мой взгляд, это доказывает, что код после await (может быть) запущен в разных потоках. И это причина того, что код быстрее (на мой взгляд, конечно).

Ответы [ 4 ]

3 голосов
/ 10 июля 2020

Важно понимать, как работают методы async.

Во-первых, они начинают работать синхронно, в одном потоке, как и любой другой метод. Без этой строки await Task.Delay(1) компилятор предупредит вас, что метод будет полностью синхронным. Ключевое слово async само по себе не делает ваш метод асинхронным. Он просто позволяет использовать await.

Волшебство c происходит при первом await, который действует на неполный Task. В этот момент метод возвращает . Он возвращает Task, который вы можете использовать для проверки завершения остальной части метода.

Итак, когда у вас есть await Task.Delay(1), метод возвращается в этой строке, позволяя вашему методу MainAsync перейти к следующей строке и начать следующий вызов Calc.

Как выполняется продолжение Calc (все после await Task.Delay(1)), зависит от наличия «контекста синхронизации». Например, в ASP. NET (не Core) или в приложении пользовательского интерфейса контекст синхронизации управляет тем, как выполняются продолжения, и они будут выполняться одно за другим. В приложении пользовательского интерфейса он будет в том же потоке, из которого начался. В ASP. NET это может быть другой поток, но все же один за другим. Таким образом, в любом случае вы не увидите никакого параллелизма.

Однако, поскольку это консольное приложение, которое не имеет контекста синхронизации, продолжения происходят в любом потоке ThreadPool, как только Task из Task.Delay(1) завершается. Это означает, что каждое продолжение может происходить параллельно.

Также стоит отметить: начиная с C# 7.1 вы можете сделать свой Main метод async, устраняя необходимость в вашем MainAsync методе:

static async Task Main(string[] args)
1 голос
/ 10 июля 2020

непрофессиональная версия ....

ничто в процессе не возвращает процессорное время без 'задержки', и поэтому он не дает никакого другого процессорного времени, вы сбиваете с толку это с многопоточным кодом. «asyn c and await» - это не о многопоточности, а об использовании ЦП (поток / потоки), когда он выполняет не работу ЦП, то есть запись на диск. Запись на диск не нуждается в потоке (ЦП). Поэтому, когда что-то асинхронно c, оно может освободить поток и использоваться для чего-то другого вместо ожидания завершения не ЦП (задача oi).

@ sunside говорит то же самое, только более технически.

static async Task<int> Calc(int a)
{
    //faking a asynchronous .... this will give this thread to something else 
    // until done then return here...
    // does not make sense... as your making this take longer for no gain.
    await Task.Delay(1);


    Console.WriteLine("Calc started");

    int result = 0;
   
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }

    return result;
}

vs

static async Task<int> Calc(int a)
{
    
    using (var reader = File.OpenText("Words.txt"))
    {
        //real asynchronous .... this will give this thread to something else 
        var fileText = await reader.ReadToEndAsync();
        // Do something with fileText...
    }
        
    Console.WriteLine("Calc started");

    int result = 0;
   
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }

    return result;
}

Причина, по которой он выглядит «параллельным», заключается в том, что он просто дает другим задачам процессорное время.

пример; он же без задержки

  • await a; do this (фактический aysn c работа)
  • await b; do this (без фактического aysn c работа)
  • ожидание c; сделать это (нет фактического aysn c работа)

пример 2; он же с задержкой

  • ждать ; запустите это, затем приостановите это (поддельный asyn c), начните b, но вернитесь и завершите sh a
  • await b; начните это, затем приостановите это (поддельный asyn * 105 5 *), запускаем c, но возвращаемся и заканчиваем sh b
  • await c; начните это, затем приостановите это (поддельный asyn c), вернитесь и завершите sh c

то, что вы должны найти, хотя больше запускается раньше, общее время будет больше, так как это как бесполезное переключение между задачами с фальшивой асинхронной задачей. где, как, если бы await Task.Delay (1) был реальной асинхронной функцией c, известной как асинхронная по своей природе, тогда преимущество было бы в том, что она могла бы начать другую работу, используя поток, который был бы заблокирован ... пока он ждет что-то, что не требует потока.

update глупый код, чтобы показать его медленнее ... Убедитесь, что вы находитесь в режиме «Release», вы всегда должны игнорировать первый запуск ... test глупы, и вам нужно будет использовать https://github.com/dotnet/BenchmarkDotNet, чтобы действительно увидеть разницу

static void Main(string[] args)
{
    Console.WriteLine("Exmaple1 - no Delay, expecting it to be faster, shorter times on average");
    for (int i = 0; i < 10; i++)
    {
        Exmaple1().GetAwaiter().GetResult();
    }

    Console.WriteLine("Exmaple2- with Delay, expecting it to be slower,longer times on average");
    for (int i = 0; i < 10; i++)
    {
        Exmaple2().GetAwaiter().GetResult();
    }
}


static async Task Exmaple1()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Task<int> a = Calc1(18000); await a;
    Task<int> b = Calc1(18000); await b;
    Task<int> c = Calc1(18000); await c;
    stopwatch.Stop();
    Console.WriteLine("Time elapsed: {0}", stopwatch.Elapsed);
}

static async Task<int> Calc1(int a)
{
    int result = 0;
    for (int k = 0; k < a; ++k) { for (int l = 0; l < a; ++l) { result += l; } }
    return result;
}

static async Task Exmaple2()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Task<int> a = Calc2(18000); await a;
    Task<int> b = Calc2(18000); await b;
    Task<int> c = Calc2(18000); await c;
    stopwatch.Stop();
    Console.WriteLine("Time elapsed: {0}", stopwatch.Elapsed);
}

static async Task<int> Calc2(int a)
{
    await Task.Delay(1);
    int result = 0;
    for (int k = 0; k < a; ++k){for (int l = 0; l < a; ++l) { result += l; } }
    return result;
}
1 голос
/ 10 июля 2020

Функция asyn c возвращает незавершенную задачу вызывающей стороне при ее первом незавершенном await. После этого await на вызывающей стороне будет ожидать завершения этой задачи.

Без await Task.Delay(1), Calc() не имеет собственных ожиданий, поэтому вернется к вызывающему только тогда, когда он бежит до конца. На этом этапе возвращенный Task уже завершен, поэтому await на вызывающем сайте немедленно использует результат без фактического вызова механизма asyn c.

0 голосов
/ 10 июля 2020

Используя шаблон async / await, вы намереваетесь, чтобы ваш метод Calc запускался как задача:

Task<int> a = Calc(18000);

Нам нужно sh установить, что задачи обычно асинхронные по своей природе, но не parallel - параллелизм - это особенность потоков. Однако под капотом некоторый поток будет использоваться для выполнения ваших задач. В зависимости от контекста, в котором вы запускаете свой код, несколько задач могут или не могут выполняться параллельно или последовательно, но они будут (разрешены) асинхронными.

Хороший способ усвоить это - представить учителя (поток) отвечая на вопрос (задачи) в классе. Учитель никогда не сможет ответить на два разных вопроса одновременно - т. Е. Параллельно - но он сможет ответить на вопросы нескольких учеников, а также может быть прерван новыми вопросами между ними.

В частности, async / await - это функция совместной многопроцессорной обработки (акцент на «кооперативности»), при которой задачи могут быть запланированы в потоке, только если этот поток свободен - и если какая-то задача уже выполняется в этом потоке, она должна вручную уступить дорогу. (Наличие и количество потоков, доступных для выполнения, в свою очередь, зависит от среды, в которой выполняется ваш код.)

При запуске Task.Delay(1) код сообщает, что он намеревается засыпать, что сигнализирует планировщик , который может выполнить другая задача, тем самым обеспечивая асинхронность. То, как он используется в вашем коде, по сути, является немного худшей версией Task.Yield (вы можете найти более подробную информацию об этом в комментариях ниже).

В общем, всякий раз, когда вы await, метод, который в данный момент выполняется, "возвращается", помечая код после него как продолжение. Это продолжение будет выполнено только тогда, когда планировщик задач снова выберет текущую задачу для выполнения. Это может произойти, если никакая другая задача в настоящее время не выполняется и не ожидает выполнения - например, потому что все они дали результат или await что-то «длительное».

В вашем примере метод Calc дает результат из-за Task.Delay и выполнение возвращается к методу Main. Это, в свою очередь, входит в следующий метод Calc, и шаблон повторяется. Как было установлено в других ответах, эти продолжения могут выполняться или не выполняться в разных потоках, в зависимости от среды - без контекста синхронизации (например, в консольном приложении) это может произойти . Для ясности: это не функция Task.Delay или async / await, а конфигурация, в которой выполняется ваш код. Если вам требуется параллелизм , используйте соответствующие потоки или убедитесь, что ваши задачи запускаются таким образом, что они поощряют использование нескольких потоков.

В другом примечании: всякий раз, когда вы намереваетесь запускать синхронный код в асинхронном режиме, используйте Task.Run() для его выполнения. Это гарантирует, что он не будет слишком мешать вам, всегда используя фоновый поток. Этот SO-ответ на LongRunning задачах может быть поучительным.

...