Так как оригинальная статья довольно объемная, я взял на себя смелость разбить ее на несколько независимых частей, более легких для перевода и восприятия. Disclaimer: Я не являюсь профессиональным переводчиком, перевод подготовлен скорее для себя и коллег. Я буду благодарен за любые исправления и помощь в переводе, статья очень интересная давайте сделаем её доступной на русском языке. Часть 1: В самом начале… Часть 2: Асинхронная модель на основе событий (EAP) Часть 3: Появление Tasks (Асинхронная модель на основе задач (TAP) Часть 4: ...и ValueTasks Часть 5: Итераторы C# в помощь Часть 6: Async/await: Внутреннее устройство Часть 7: SynchronizationContext и ConfigureAwait и поля в State Machine В .NET Framework 4.0 появился тип System.Threading.Tasks.Task. По своей сути Task - это просто структура данных, которая представляет собой возможное завершение некоторой асинхронной операции (в других платформах аналогичный тип называется  «promise» или «future»). Task создается для представления некоторой операции, а затем, когда операция, которую она логически представляет, завершается, результаты сохраняются в Task. Достаточно просто. Но ключевая особенность Task, которая делает его гораздо полезнее IAsyncResult, заключается в том, что она встраивает в себя понятие продолжения. Эта особенность означает, что вы можете подойти к любой задаче и попросить асинхронно уведомить вас о ее завершении, причем сама задача будет обрабатывать синхронизацию, чтобы обеспечить вызов продолжения независимо от того, завершилась ли задача, еще не завершилась или завершается одновременно с запросом на уведомление. Почему это так важно? Если вы вспомните наше обсуждение старого шаблона APM, то там было две основные проблемы. Вы должны были реализовать собственную реализацию iasyncresult для каждой операции: не было встроенной реализации IAsyncResult, которую можно было бы просто использовать для своих нужд. Вы должны были знать до вызова метода Begin, что вы хотите сделать после его завершения. Это делает реализацию комбинаторов и других обобщенных процедур для потребления и составления произвольных асинхронных реализаций весьма сложной задачей.  В отличие от Task, это общее представление позволяет вам подойти к асинхронной операции после того, как вы уже инициировали операцию, и предоставить продолжение после того, как вы уже инициировали операцию... вам не нужно предоставлять это продолжение методу, который инициирует операцию. Каждый, кто имеет асинхронные операции, может производить Task, а каждый, кто потребляет асинхронные операции, может потреблять Task, и для связи между ними не нужно делать ничего особенного: Task становится универсальным языком для общения производителей и потребителей асинхронных операций. И это изменило лицо .NET. Подробнее об этом чуть позже... Вместо того чтобы погружаться в сложный код Task, мы поступим по-педагогически и просто реализуем простую версию. Это не претендует на большую реализацию, а лишь достаточно полная функциональность, чтобы помочь понять суть Task, которая, в конце концов, является просто структурой данных, координирующей подачу и прием сигнала о завершении. Мы начнем всего с нескольких полей:   Нам нужно поле, чтобы знать, завершилась ли задача (_completed), и нам нужно поле для хранения любой ошибки, которая привела к неудаче задачи (_error); если бы мы также реализовывали общий MyTask<TResult>, было бы также private TResult _result для хранения успешного результата операции. Пока что это очень похоже на нашу пользовательскую реализацию IAsyncResult (не случайно, конечно). Но теперь самое главное - поле _continuation. В этой простой реализации мы поддерживаем только одно продолжение, но этого достаточно для пояснения (в настоящей Task используется поле object, которое может быть либо отдельным объектом продолжения, либо List<> объектов продолжения). Это делегат, который будет вызван, когда задача завершится. Теперь немного о сути. Как уже отмечалось, одним из фундаментальных достижений Task по сравнению с предыдущими моделями была возможность предоставлять работу по продолжению (обратный вызов) после того, как операция была инициирована. Нам нужен метод, который позволит нам это сделать, поэтому добавим ContinueWith: Если к моменту вызова ContinueWith задача уже была помечена как завершенная, ContinueWith просто ставит в очередь выполнение делегата. В противном случае метод сохраняет делегат, так что продолжение может быть поставлено в очередь, когда задача завершится (он также сохраняет нечто, называемое ExecutionContext, и затем использует это при последующем вызове делегата, но пока не беспокойтесь об этой части... мы к ней еще вернемся). Все достаточно просто. Затем нам нужно иметь возможность пометить MyTask как завершенную, что означает, что асинхронная операция, которую она представляет завершилась. Для этого мы реализуем два метода, один для отметки успешного завершения ("SetResult"), а другой для отметки завершения с ошибкой ("SetException"): Мы сохраняем любую ошибку, помечаем задачу как выполненную, а затем, если ранее было зарегистрировано продолжение, ставим его в очередь на вызов. Наконец, нам нужен способ распространить любое исключение, которое могло произойти в задаче (и, если бы это была типовая MyTask, вернуть ее _result); для поддержки определенных сценариев мы также разрешаем этому методу блокировать ожидание завершения задачи, что мы можем реализовать в терминах ContinueWith (продолжение просто сигнализирует ManualResetEventSlim, который вызывающая сторона затем блокирует в ожидании завершения). И это, по сути, все. Конечно, настоящая Task намного сложнее, с гораздо более эффективной реализацией, с поддержкой любого количества продолжений, с множеством настроек о том, как она должна себя вести (например. должны ли продолжения ставиться в очередь, как это сделано здесь, или они должны вызываться синхронно как часть завершения задачи), с возможностью хранить несколько исключений, а не только одно, со специальными знаниями об отмене, с тоннами вспомогательных методов для выполнения общих операций (например, Task.Run, который создает Task для представления делегата, поставленного в очередь для вызова на пуле потоков) и так далее. Но во всем этом нет никакой магии; в своей основе это просто то, что мы видели здесь. Вы также можете заметить, что у моей простой MyTask есть публичные методы SetResult/SetException непосредственно на ней, в то время как у Task их нет. На самом деле у Task есть такие методы, просто они внутренние, с типом System.Threading.Tasks.TaskCompletionSource, служащим отдельным "производителем" для задачи и ее завершения; это было сделано не из технической необходимости, а как способ удержать методы завершения от вещи, предназначенной только для потребления. Вы можете передавать задачу, не беспокоясь о том, что она будет завершена из-под вашего контроля; сигнал завершения является деталью реализации того, что создало задачу, а также оставляет за собой право завершить ее, оставляя источник TaskCompletionSource при себе. (CancellationToken и CancellationTokenSource работают по аналогичной схеме: CancellationToken - это просто структурная обертка для CancellationTokenSource, предоставляющая только публичную доступную область, связанную с потреблением сигнала отмены, но без возможности его создания, которая является возможностью, ограниченной тем, кто имеет доступ к CancellationTokenSource). Конечно, мы можем реализовать комбинаторы и помощники для этой MyTask, аналогичные тем, что предоставляет Task. Хотите простой MyTask.WhenAll? Вот, пожалуйста: Хотите MyTask.Run? Он у вас есть: Как насчет MyTask.Delay? Конечно: Вы поняли идею. С появлением Task все предыдущие асинхронные паттерны в .NET ушли в прошлое. Везде, где асинхронная реализация ранее была реализована с помощью паттерна APM или EAP, появились новые методы, возвращающие Task.