В C# появилась конструкция async/await. Далее показан вариант как добиться подобного поведения на Delphi.

Я предпологаю, что вы знакомы с async/await. Её удобно использовать для операций, где участие процессора не требуется. Процессор только начинает операцию, а из внешнего мира приходит сообщают об её окончании. Хорошим примером может служить вызов на веб сервер. Допустим наш веб сервер умеет выполнять две операции: сложение и умножение.
        async Task<int> MakeAsyncTask_Plus(int aArg1, int aArg2, int aDurationMs)
        {   // эмуляция вызова на веб сервер
            await Task.Delay(aDurationMs);
            return aArg1 + aArg2;
        }
        async Task<int> MakeAsyncTask_Mult(int aArg1, int aArg2, int aDurationMs)
        {   // эмуляция вызова на веб сервер
            await Task.Delay(aDurationMs);
            return aArg1 * aArg2;
        }

Клиент хочет вычислить выражение (1+2)*(3+4). Т.к. результаты сложений независимы, их можно выполнять одновременно:
        async void CalcAsync()
        {
            Task<int> aPlus1 = MakeAsyncTask_Plus(1, 2, 2000);      // 1
            Task<int> aPlus2 = MakeAsyncTask_Plus(3, 4, 2000);      // 2
 
            int aArg1 = await aPlus1;                                // 3
            int aArg2 = await aPlus2;                                // 4
 
            Task<int> aMult = MakeAsyncTask_Mult(aArg1, aArg2, 1000); // 5
            int aRes = await aMult;                                  // 6
            Log(string.Format("{0} * {1} = {2}", aArg1, aArg2, aRes));
        }

До строки "//3"(первого await) метод отрабатывает обычным образом. На await происходит выход из метода, а продолжится (до следующего await) по окончанию операции. Управление передастся механизмом сообщений или в другом потоке — настраивается в окружении. В C# это достигается возможностями компилятора. В Delphi похожего поведения на одном потоке можно достичь при помощи Fiber. Выглядеть подобный метод будет похоже:
procedure TCalcAsync.Execute;
var
  aPlus1: TAsyncTask<Integer>;
  aPlus2: TAsyncTask<Integer>;
  aMult: TAsyncTask<Integer>;
  aArg1, aArg2: Integer;
  aRes: Integer;
begin
  aPlus1 := nil;
  aPlus2 := nil;
  aMult := nil;
  try
    aPlus1 := TAsyncTask_Plus.Create(Self, 1,{+}2, 2000{ms});
    aPlus2 := TAsyncTask_Plus.Create(Self, 3,{+}4, 2000{ms});
 
    aArg1 := aPlus1.Await;
    aArg2 := aPlus2.Await;
 
    aMult := TAsyncTask_Mult.Create(Self, aArg1,{*}aArg2, 1000{ms});
    aRes := aMult.Await;
    Example.Log('%d * %d = %d', [aArg1, aArg2, aRes]);
  finally
    aMult.Free;
    aPlus2.Free;
    aPlus1.Free;
  end;
end;

Пример работает для Delphi 2007, XE2, XE8.

Комментарии (17)


  1. Razaz
    10.03.2016 18:02
    -4

    Странный у вас C# код:)

            async Task CalcAsync()
            {
                var aArg1  = await AddAsync(1, 2, 2000);      
                var aArg2  = await AddAsync(3, 4, 2000);      
    
                var aRes = MultiplyAsync(aArg1, aArg2, 1000); 
    
                Log($"{aArg1} * {aArg2} = {aRes}");
            }

    Расскажите поподробнее про возврат в текущий поток и как передается управление. Как это внутри сделано? Используется IOCP?


    1. leschenko
      10.03.2016 18:05
      +3

      Код на C# как раз нормальный. А вот на Delphi не сделано ровным счетом ничего. Визуально похоже, но работает совершенно не так.


      1. Razaz
        10.03.2016 20:36

        Видимо кого-то задело, что это все в 4 строки пишется, но код и в самом деле выглядит притянутым за уши для аналогии с Delphi. И как раз был вопрос про то как это работает.


        1. leschenko
          10.03.2016 21:27

          Так ведь весь прикол в том, что оно не работает. В C# между await'ами UI поток свободен и может выполнять другие задачи, а код Delphi вынес обработку в другой поток, но UI поток занят.


          1. Razaz
            10.03.2016 22:28

            Ну дак я и спрашиваю автора дальше об этом:

            Управление передастся механизмом сообщений или в другом потоке — настраивается в окружении

            Все это выглядит как Task.Result, но были упомянуты какие-то сообщения..


            1. lair
              11.03.2016 00:03
              +1

              Все это выглядит как Task.Result, но были упомянуты какие-то сообщения..

              Ну вообще, SynchronizationContext.Post — это вполне себе сообщение, а именно так работает постановка continuation на определенный контекст (по умолчанию в await это включено).


              1. Razaz
                12.03.2016 13:14

                Ну и хотелось бы узнать как точно то работает на Delphi :)


                1. lair
                  12.03.2016 13:17

                  Из вашего коммента не очевидно, к какому коду был вопрос. Судя по количеству ответов — не мне одному.


                  1. Razaz
                    12.03.2016 14:44

                    "Расскажите поподробнее про возврат в текущий поток и как передается управление. Как это внутри сделано? Используется IOCP?"


                    1. lair
                      12.03.2016 14:45

                      … и все это под C#-ным кодом. Вот вам и рассказывают про C#-ный код.


                      1. Razaz
                        12.03.2016 15:20

                        Я просто отформатировал код автора, что бы не пугало людей, которые с C# не сильно знакомы :) Извиняюсь ели ввел в заблуждение.


          1. SuvAlexander
            14.03.2016 11:48
            +2

            1) Покажите, пожалуйста, строчку кода, которая выполняется не в главном потоке.
            2) Откуда информация, что главный поток занят? Что вам мешает повторно нажать на кнопочку "Async" во время "вычислений"?

            Добавил логирование сообщения на выход из обработчика нажатия:
            procedure TExample.btStartAsyncClick(Sender: TObject);
            begin
            // mmLog.Clear;
            TCalcAsync.Create.Switch;
            Log('btStartAsyncClick finish', []);
            end;

            Дважды быстро нажал на "Async". Получил лог (обратите внимание на положение этого сообщения — оно между сообщениями из TCalcAsync.Execute):

            1 + 2 start
            3 + 4 start
            btStartAsyncClick finish
            1 + 2 start
            3 + 4 start
            btStartAsyncClick finish
            1 + 2 = 3
            3 + 4 = 7
            3 7 start
            1 + 2 = 3
            3 + 4 = 7
            3
            7 start
            3 7 = 21
            3
            7 = 21


            1. leschenko
              14.03.2016 16:07

              Извиняюсь. Не туда посмотрел. Действительно, UI не занят.

              Но все равно так писать — извражение.


            1. lair
              14.03.2016 16:24

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

              Вы про свой код или про C#?


    1. mayorovp
      11.03.2016 09:06

      Оператор await работает следующим образом:

      1. Проверяет, не выполнена ли уже задача. Если выполнена — то у нее просто извлекается значение (или исключение) и больше ничего не делается.
      2. Захватывает текущий контекст синхронизации или планировщик задач.
      3. У задачи вызывается метод ContinueWith, в который передается лямбда, указывающая на шаг 5.
      4. Управление из метода возвращается.

      5. (Вызывается асинхронно после выполнения задачи) Если на шаге 2 был захвачен контекст синхронизации — то шаг 8 алгоритма вызывается в нем асинхронно при помощи метода Post
      6. Если на шаге 2 баз захвачен планировщик задач — то шаг 8 алгоритма запускается в нем как задача
      7. если на шаге 2 ничего захвачено не было — то шаг 8 выполняется сразу же

      8. Продолжается выполнение метода, начиная с текущего оператора await (т.е. с шага 1).

      Реально там все еще сложнее — главным образом, из-за необходимости повторно выбрасывать исключения с сохранением стектрейса.

      Большую часть реализации можно увидеть начиная отсюда: http://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/AsyncMethodBuilder.cs,108

      Также реализация await упрощенна расписана в комментарии к файлу http://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/TaskAwaiter.cs

      Магия шагов 1 и 8 (возобновление метода) реализуется компилятором.


      1. Razaz
        12.03.2016 13:15

        Зачем вы мне про C# И .Net? Это я и так знаю. Вопрос был к автору — как именно это реализовано на Delphi.


        1. leschenko
          12.03.2016 14:22

          Никак. Это не реализовано.