Базовый вариант использования Task Unwrap
- среда, 10 апреля 2024 г. в 00:00:06
Введение
Недавно, после нескольких месяцев отсутствия использования .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, который обрабатывает случай возврата обещания определенным образом: обещание, возвращенное к тому времени, будет разрешено только тогда, когда будет выполнено базовое обещание.
Чтобы получить поведение обещаний 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.