[Перевод] Базовый вариант использования Task Unwrap
Введение
Недавно, после нескольких месяцев отсутствия использования .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.