Введение

Недавно, после нескольких месяцев отсутствия использования .Net/C#, я улучшал существующее приложение .Net/C# WPF, используя .Net Task Parallel Library (TPL).

Но, наивно применяя шаблоны обещаний JavaScript, которые я использовал в предыдущие месяцы, я столкнулся со странной проблемой, которая заставила меня использовать довольно экзотический метод расширения Unwrap.

В этой статье описывается проблема, объясняется ее причина, предлагается исправление с помощью Unwrap и, наконец, представлена ​​более современная версия с парадигмой async/await C# 5.0.

Простой рабочий процесс в JavaScript с Promises

Вот JavaScript-реализация простого рабочего процесса, состоящего из трех шагов, второй из которых имитирует отложенную обработку с помощью setTimeout с использованием Promise API:

function doFirstThing() {
	return new Promise(resolve => {
		console.log("First thing done")
		resolve()
	})
}

function doSecondThing() {
	return new Promise(resolve => {
		setTimeout(() => {
			console.log("Second thing done")
			resolve()
		}, 1000)
	})
}

function doThirdThing() {
	return new Promise(resolve => {
		console.log("Third thing done")
		resolve()
	})
}

doFirstThing().then(doSecondThing).then(doThirdThing)

Вот результат после запуска с Node:

$ node test.js
First thing done
Second thing done
Third thing done

Реализация C# с задачами

Вот тот же рабочий процесс, реализованный на C# с использованием .Net TPL:

using System;
using System.Threading.Tasks;

namespace Test
{
    class Program
    {
        static Task DoFirstThing()
        {
            return Task.Run(() => Console.WriteLine("First thing done"));
        }

        static Task DoSecondThing()
        {
            return Task.Delay(1000).ContinueWith(_ => Console.WriteLine("Second thing done"));
        }

        static Task DoThirdThing()
        {
            return Task.Run(() => Console.WriteLine("Third thing done"));
        }

        static void Main(string[] args)
        {
            DoFirstThing().ContinueWith(_ => DoSecondThing()).ContinueWith(_ => DoThirdThing());

            Console.ReadLine();
        }
    }
}

Обратите внимание, что в отличие от обещаний JavaScript, задачи .Net не запускаются/планируются автоматически при создании, поэтому необходимо явно вызывать Run.

Here is the result:

First thing done
Third thing done
Second thing done

Как видите, третий шаг выполняется раньше второго!

Это связано с тем, что ContinueWith создает новую задачу, обертывающую предоставленную обработку, которая состоит только в вызове DoSecondThing (который сам создает вторую задачу), который немедленно возвращает результат.

ContinueWith не будет учитывать результирующую задачу, в отличие от Promise.then, который обрабатывает случай возврата обещания определенным образом: обещание, возвращенное к тому времени, будет разрешено только тогда, когда будет выполнено базовое обещание.

Unwrap в помощь

Чтобы получить поведение обещаний JavaScript, нам нужно явно сообщить TPL, что мы хотим рассмотреть базовую задачу, используя Unwrap (реализованный как метод расширения, предоставляемый классом TaskExtensions):

DoFirstThing()
  .ContinueWith(_ => DoSecondThing())
  .Unwrap()
  .ContinueWith(_ => DoThirdThing());

Результат теперь соответствует JavaScript:

First thing done
Second thing done
Third thing done

Более современный способ реализации await

В C# 5.0 добавлен некоторый синтаксический сахар, чтобы упростить использование TPL с оператором ожидания (await operator ):

await DoFirstThing();
await DoSecondThing();
await DoThirdThing();

await внутренне вызывает Unwrap и ожидает выполнения базовой задачи, как и ожидалось, и дает тот же результат.

Обратите внимание, что await можно использовать только в асинхронном методе.

Заключение

Сопоставление между языками и платформами не всегда очевидно, но, к счастью, в настоящее время все они копируют друг друга и в конечном итоге предлагают одни и те же парадигмы и API, такие как дуэт async/await, который вы используете почти одинаково как в C#, так и в JavaScript.

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


  1. rukhi7
    09.04.2024 05:41
    +5

    после нескольких месяцев отсутствия использования .Net/C#

    это прям вот сразу ужас какой-то!

    А по содержанию, понятно что это перевод, но посмотрите что вам предлогают:

    DoFirstThing().ContinueWith(_ => DoSecondThing()).ContinueWith(_ => DoThirdThing());

    Это называется "современный стиль кодирования", я так понимаю = пишите так чтобы никто ничего не понял, пихайте как можно больше вызовов функций в одну строчку, чем больше напихали, тем современнее!

    Например тут в зависимости от того как считать лямбды или 5 или 7 вызовов в одной строчке, не особо впечатляет.


  1. Omankit
    09.04.2024 05:41
    +2

    Это перевод статьи из 2012 года?
    Не пишите такой код как в статье.


    1. mayorovp
      09.04.2024 05:41
      +2

      Нет, 2019го. Но тем хуже для автора.


  1. maksim_sitnikov
    09.04.2024 05:41

    Типичный is, но в джс асинк появился раньше чем в питоне, но в последнем в отличии от джс , да и в других языках асинк это внатуре асинк, а не промисы как последовательная очередь заданий. Кста в последней строке джс кода всё встанет и будет ждать результата, уж если это какая-то паралельная работа код должен не стоять, а ехать, а для ожидания результата нужен обработчик. Цепочку можно как написано выше в первом коментарии или task. When ещё например, куча вариантов task from result


  1. Okunev_PY
    09.04.2024 05:41

    Код не эквивалентен ни разу.

    Js вызываеться последовательно цепочкой then.

    Эквивалентный вызов был бы.

    DoFirstThing().ContinueWith(_ => DoSecondThing().ContinueWith(_ => DoThirdThing()));

    Перестановка одной скобки в корень бы поменяло поведение. И никакой UnWrap вам тут был бы не нужен.


    1. mayorovp
      09.04.2024 05:41

      С чего бы цепочка then должна быть эквивалентной вложенным ContinueWith? Это во-первых.

      А это-вторых, если потребуется вернуть Task (а его требуется возвращать почти всегда во избежание утери обратного давления) - вам снова понадобятся Unwrap.


      1. rukhi7
        09.04.2024 05:41

        С чего бы цепочка then должна быть эквивалентной вложенным ContinueWith?

        интересно а кто по вашему определяет эквивалентность? Это должно быть чье-то мнение по вашему???

        По моему, объективным критерием является эквивалентность поведения цепочек, раз ПОВЕДЕНИЕ вложенных ContinueWith эквивалентно поведению цепочки then можно утверждать что эти конструкции разных языков эквивалентны, мне кажется.

        Так как вы определяете эквивалентность?


        1. mayorovp
          09.04.2024 05:41

          Так же, по поведению.

          then возвращает плоский Promise
          вложенный ContinueWith не возвратит вам плоский Task


          1. rukhi7
            09.04.2024 05:41

            then возвращает плоский Promise

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

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


            1. mayorovp
              09.04.2024 05:41

              В данном случае это вы зачем-то лезете в бутылку и игнорируете второй аспект вопроса.

              Что делать, если цепочка на этом месте не закончилась, и должна быть продолжена за пределами метода?

              Task Foo()
              {
                  DoFirstThing().ContinueWith(_ => DoSecondThing().ContinueWith(_ => DoThirdThing()));
              
                  return ???;
              }
              

              Вы, конечно, можете сказать: опять я неправильно применил эквивалентность, и цепочку надо растить вглубь:

              void Foo(Action<Task> next)
              {
                  DoFirstThing().ContinueWith(_ => DoSecondThing().ContinueWith(_ => DoThirdThing().ContinueWith(next)));
              }
              

              Да, так будет работать, но в чём тут вообще смысл использования Task?


              1. rukhi7
                09.04.2024 05:41

                Да, так будет работать, но в чём тут вообще смысл использования Task?

                ну насколько я понимаю, смысл в том чтобы продемонстрировать как написать код так чтобы задания 1,2,3 завершались в правильном порядке, поскольку мы не знаем зачем нам чтобы они завершелись в правильном порядке, дальше(!) смысл искать бесполезно!

                с точки зрения поддержания этого порядка цепочка then будет эквивалентной вложенным ContinueWith никуда лезьть не надо!

                Какой вы еще хотите смысл найти? Какая разница какие там таски-промисы плоские, квадратные, зеленые... ? Или вы видите некоторую глобальность в этой примитивном вопросе организации завершений? Если так, то я этой глобальности взгляда и анализа разделить с вами не смогу, извините, для меня все просто.


              1. rukhi7
                09.04.2024 05:41

                прежде чем уводить дисскуссию в какие-то дебри о плоских (квадратных, зеленых, еще каких?) промисах-тасках, вы на простой вопрос попробуйте сначала ответить:

                Чем плохо что задачи будут завершаться в порядке 1,3,2 ;

                а не в порядке 1,2,3 ? Почему это надо исправлять? Есть у вас ответ в данном конкретном случае? Я могу конечно предположения делать, но предположения ничего не доказывают!


  1. P40b0s
    09.04.2024 05:41
    +1

    В c#5 добавлен await? Вот это новости)