Введение
Недавно, после нескольких месяцев отсутствия использования .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)
maksim_sitnikov
09.04.2024 05:41Типичный is, но в джс асинк появился раньше чем в питоне, но в последнем в отличии от джс , да и в других языках асинк это внатуре асинк, а не промисы как последовательная очередь заданий. Кста в последней строке джс кода всё встанет и будет ждать результата, уж если это какая-то паралельная работа код должен не стоять, а ехать, а для ожидания результата нужен обработчик. Цепочку можно как написано выше в первом коментарии или task. When ещё например, куча вариантов task from result
Okunev_PY
09.04.2024 05:41Код не эквивалентен ни разу.
Js вызываеться последовательно цепочкой then.
Эквивалентный вызов был бы.
DoFirstThing().ContinueWith(_ => DoSecondThing().ContinueWith(_ => DoThirdThing()));
Перестановка одной скобки в корень бы поменяло поведение. И никакой UnWrap вам тут был бы не нужен.
mayorovp
09.04.2024 05:41С чего бы цепочка then должна быть эквивалентной вложенным ContinueWith? Это во-первых.
А это-вторых, если потребуется вернуть Task (а его требуется возвращать почти всегда во избежание утери обратного давления) - вам снова понадобятся Unwrap.
rukhi7
09.04.2024 05:41С чего бы цепочка then должна быть эквивалентной вложенным ContinueWith?
интересно а кто по вашему определяет эквивалентность? Это должно быть чье-то мнение по вашему???
По моему, объективным критерием является эквивалентность поведения цепочек, раз ПОВЕДЕНИЕ вложенных ContinueWith эквивалентно поведению цепочки then можно утверждать что эти конструкции разных языков эквивалентны, мне кажется.
Так как вы определяете эквивалентность?
mayorovp
09.04.2024 05:41Так же, по поведению.
then возвращает плоский Promise
вложенный ContinueWith не возвратит вам плоский Taskrukhi7
09.04.2024 05:41then возвращает плоский Promise
эквивалентность очевидно не может быть полной потому что сравниваются разные языки, если следовать вашей логике то можно вообще залезть в бутылку и сказать что они не эквивалентны потому что один возвращает Промис, а другой Таск.
Поэтому... вряд ли это можно считать конструктивным аргументом.
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?
rukhi7
09.04.2024 05:41Да, так будет работать, но в чём тут вообще смысл использования Task?
ну насколько я понимаю, смысл в том чтобы продемонстрировать как написать код так чтобы задания 1,2,3 завершались в правильном порядке, поскольку мы не знаем зачем нам чтобы они завершелись в правильном порядке, дальше(!) смысл искать бесполезно!
с точки зрения поддержания этого порядка цепочка then будет эквивалентной вложенным ContinueWith никуда лезьть не надо!
Какой вы еще хотите смысл найти? Какая разница какие там таски-промисы плоские, квадратные, зеленые... ? Или вы видите некоторую глобальность в этой примитивном вопросе организации завершений? Если так, то я этой глобальности взгляда и анализа разделить с вами не смогу, извините, для меня все просто.
rukhi7
09.04.2024 05:41прежде чем уводить дисскуссию в какие-то дебри о плоских (квадратных, зеленых, еще каких?) промисах-тасках, вы на простой вопрос попробуйте сначала ответить:
Чем плохо что задачи будут завершаться в порядке 1,3,2 ;
а не в порядке 1,2,3 ? Почему это надо исправлять? Есть у вас ответ в данном конкретном случае? Я могу конечно предположения делать, но предположения ничего не доказывают!
rukhi7
это прям вот сразу ужас какой-то!
А по содержанию, понятно что это перевод, но посмотрите что вам предлогают:
DoFirstThing().ContinueWith(_ => DoSecondThing()).ContinueWith(_ => DoThirdThing());
Это называется "современный стиль кодирования", я так понимаю = пишите так чтобы никто ничего не понял, пихайте как можно больше вызовов функций в одну строчку, чем больше напихали, тем современнее!
Например тут в зависимости от того как считать лямбды или 5 или 7 вызовов в одной строчке, не особо впечатляет.