Содержание
- Асинхронные методы, async и await
- Не блокировать, а использовать await Don’t block, await instead
- Одновременный запуск задач Start tasks concurrently
- Сочетаемость задач Composition with tasks
- Эффективное ожидание задач Await tasks efficiently
Асинхронные методы, async и await
Асинхронность позволяет вынести отдельные задачи из основного потока в специальные асинхронные методы или блоки кода. Особенно это актуально в графических программах, где продолжительные задачи могу блокировать интерфейс пользователя. И чтобы этого не произошло, нужно задействовать асинхронность. Также асинхронность несет выгоды в веб-приложениях при обработке запросов от пользователей, при обращении к базам данных или сетевым ресурсам. При больших запросах к базе данных асинхронный метод просто уснет на время, пока не получит данные от БД, а основной поток сможет продолжить свою работу. В синхронном же приложении, если бы код получения данных находился в основном потоке, этот поток просто бы блокировался на время получения данных.
Ключевыми для работы с асинхронными вызовами в C# являются два ключевых слова: async и await , цель которых — упростить написание асинхронного кода. Они используются вместе для создания асинхронного метода.
Асинхонный метод обладает следующими признаками:
В заголовке метода используется модификатор async
Метод содержит одно или несколько выражений await
В качестве возвращаемого типа используется один из следующих:
Асинхронный метод, как и обычный, может использовать любое количество параметров или не использовать их вообще. Однако асинхронный метод не может определять параметры с модификаторами out и ref .
Также стоит отметить, что слово async , которое указывается в определении метода, не делает автоматически метод асинхронным. Оно лишь указывает, что данный метод может содержать одно или несколько выражений await .
Рассмотрим пример асинхронного метода:
Здесь прежде всего определен обычный метод подсчета факториала. Для имитации долгой работы в нем используется задержка на 8 секунд с помощью метода Thread.Sleep() . Условно это некоторый метод, который выполняет некоторую работу продолжительное время. Но для упрощения понимания он просто подсчитывает факториал числа 6.
Также здесь определен асинхронный метод FactorialAsync() . Асинхронным он является потому, что имеет в определении перед возвращаемым типом модификатор async , его возвращаемым типом является vo >await .
Выражение await определяет задачу, которая будет выполняться асинхронно. В данном случае подобная задача представляет выполнение функции факториала:
По негласным правилам в названии асинхроннных методов принято использовать суффикс Async — Factorial Async () , хотя в принципе это необязательно делать.
Сам факториал мы получаем в асинхронном методе FactorialAsync . Асинхронным он является потому, что он объявлен с модификатором async и содержит использование ключевого слова await .
И в методе Main мы вызываем этот асинхронный метод.
Посмотрим, какой у программы будет консольный вывод:
Разберем поэтапно, что здесь происходит:
Запускается метод Main, в котором вызывается асинхронный метод FactorialAsync.
Метод FactorialAsync начинает выполняться синхронно вплоть до выражения await.
Выражение await запускает асинхронную задачу Task.Run(()=>Factorial())
Пока выполняется асинхронная задача Task.Run(()=>Factorial()) (а она может выполняться довольно продожительное время), выполнение кода возвращается в вызывающий метод — то есть в метод Main. В методе Main нам будет предложено ввести число для вычисления квадрата числа.
В этом и преимущество асинхронных методов — асинхронная задача, которая может выполняться довольно долгое время, не блокирует метод Main, и мы можем продолжать работу с ним, например, вводить и обрабатывать данные.
Когда асинхронная задача завершила свое выполнение (в случае выше — подсчитала факториал числа), продолжает работу асинхронный метод FactorialAsync, который вызвал асинхронную задачу.
Функция факториала, возможно, представляет не самый показательный пример, так как в реальности в данном случае нет смысла делать ее асинхронной. Но рассмотрим другой пример — чтение-запись файла:
Асинхронный метод ReadWriteAsync() выполнение запись в файл некоторой строки и затем считывает записанный файл. Подобные операции могут занимать продолжительное время, особенно при больших объемах данных, поэтому такие операции лучше делать асинхронными.
Фреймворк .NET уже имеет встроенную поддержку таких операций. Например, в классе StreamWriter определен метод WriteLineAsync() . По сути он уже представляет асинхронную операцию и принимает в качестве параметра некоторую строку, которую надо записать в файл. Поскольку этот метод представляет асинхронную операцию, то вызов этого метода мы можем оформить в выражение await :
Аналогично в классе StreamReader определен метод ReadToEndAsync() , который также представляет асинхронную операцию и который возвращает весь считанный текст.
Во фреймворке .NET Core определено много подобных методов. Как правило, они связаны с работой с файлами, отправкой сетевых запросов или запросов к базе данных. Их легко узнать по суффиксу Async . То есть если метод имеет подобный суффикс в названии, то с большей степенью вроятности его можно использовать в выражении await.
Далее в методе Main вызывается асинхронный метод ReadWriteAsync:
И опять же когда выполнение в методе ReadWriteAsync доходит до первого выражения await, управление возвращается в метод Main, и мы можем продолжать с ним работу. Запись в файл и считывания файла будут производиться параллельно и не будут блокировать работу метода Main.
Определение асинхронной операции
Как, выше уже было сказано, фреймворк .NET Core имеет много встроенных методов, которые представляют асинхронную операцию. Они заканчиваются на суффикс Async. И перед вызывами подобных методов мы можем указывать оператор await . Например:
Либо мы сами можем определить асинхронную операцию, используя метод Task.Run() :
Можно определить асинхронную операцию с помощью лямбда-выражения:
Передача параметров в асинхронную операцию
Выше вычислялся факториал 6, но, допустим, мы хотим вычислять факториалы разных чисел:
Получение результата из асинхронной операции
Асинхронная операция может возвращать некоторый результат, получить который мы можем так же, как и при вызове обычного метода:
Модель асинхронного программирования задач (TAP) предоставляет абстракцию асинхронного кода. The Task asynchronous programming model (TAP) provides an abstraction over asynchronous code. Вы пишете код как последовательность операторов, как обычно. You write code as a sequence of statements, just like always. Вы можете читать этот код, как если бы каждая инструкция завершалась до начала следующей. You can read that code as though each statement completes before the next begins. Компилятор выполняет ряд преобразований, так как некоторые из этих инструкций могут начать работу и вернуть Task, представляющий текущую работу. The compiler performs a number of transformations because some of those statements may start work and return a Task that represents the ongoing work.
Это и есть цель такого синтаксиса: сделать возможным код, который читается как последовательность операторов, но выполняется в гораздо более сложном порядке на основе выделения внешних ресурсов и при завершении задач. That’s the goal of this syntax: enable code that reads like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks complete. Это аналогично тому, как люди дают инструкции для процессов, которые включают асинхронные задачи. It’s analogous to how people give instructions for processes that include asynchronous tasks. В этой статье вы используете пример инструкции для приготовления завтрака, чтобы увидеть, как ключевые слова async и await упрощают понимание кода, который включает в себя серию асинхронных инструкций. Throughout this article, you’ll use an example of instructions for making a breakfast to see how the async and await keywords make it easier to reason about code that includes a series of asynchronous instructions. Можно написать инструкции аналогично следующему списку, чтобы объяснить, как приготовить завтрак. You’d write the instructions something like the following list to explain how to make a breakfast:
- Налить чашку кофе. Pour a cup of coffee.
- Нагреть сковородку, а затем поджарить два яйца. Heat up a pan, then fry two eggs.
- Поджарить три куска бекона. Fry three slices of bacon.
- Сделать два тоста. Toast two pieces of bread.
- Намазать тосты маслом и джемом. Add butter and jam to the toast.
- Налить стакан апельсинового сока. Pour a glass of orange juice.
Если у вас есть кулинарный опыт, вы бы выполняли эти инструкции асинхронно. If you have experience with cooking, you’d execute those instructions asynchronously. Сначала вы бы поставили сковородку на огонь, а затем занялись бы беконом. You’d start warming the pan for eggs, then start the bacon. Потом бы поставили тосты, а вслед за этим принялись бы за яичницу. You’d put the bread in the toaster, then start the eggs. На каждом этапе процесса необходимо запустить задачу, а затем обратить внимание на другие задачи, которые требуют вашего внимания. At each step of the process, you’d start a task, then turn your attention to tasks that are ready for your attention.
Приготовление завтрака представляет собой хороший пример асинхронной непараллельной работы. Cooking breakfast is a good example of asynchronous work that isn’t parallel. Один пользователь (или поток) может обрабатывать все эти задачи. One person (or thread) can handle all these tasks. Продолжая аналогию с завтраком, один человек может приготовить завтрак асинхронно путем запуска очередной задачи до завершения предыдущей. Continuing the breakfast analogy, one person can make breakfast asynchronously by starting the next task before the first completes. Готовка продолжается вне зависимости от того, следит ли за ней кто-либо. The cooking progresses whether or not someone is watching it. Как только вы начали греть сковороду для яичницы, можно заняться обжаркой бекона. As soon as you start warming the pan for the eggs, you can begin frying the bacon. Когда бекон будет жариться, можно поместить хлеб в тостер. Once the bacon starts, you can put the bread into the toaster.
Для параллельного алгоритма потребовалось бы несколько поваров (или потоков). For a parallel algorithm, you’d need multiple cooks (or threads). Один готовит яйца, один — бекон и т. д. One would make the eggs, one the bacon, and so on. Каждый из них будет заниматься только одной задачей. Each one would be focused on just that one task. Каждый повар (или поток) будет заблокирован синхронным ожиданием готовности бекона или тостов. Each cook (or thread) would be blocked synchronously waiting for bacon to be ready to flip, or the toast to pop.
Теперь рассмотрим эти же инструкции, написанные на C#. Now, consider those same instructions written as C# statements:
Компьютеры не рассматривают эти инструкции так же, как люди. Computers don’t interpret those instructions the same way people do. Компьютер будет задерживаться над каждой инструкцией до момента, когда работа будет завершена, прежде чем перейдет к следующему оператору. The computer will block on each statement until the work is complete before moving on to the next statement. Вряд ли такой завтрак вас устроит. That creates an unsatisfying breakfast. Более поздние задачи не будут начаты до завершения предыдущих. The later tasks wouldn’t be started until the earlier tasks had completed. Потребуется гораздо больше времени для приготовления завтрака, к тому же часть уже остынет еще до подачи. It would take much longer to create the breakfast, and some items would have gotten cold before being served.
Если требуется, чтобы компьютер асинхронно выполнил инструкции выше, необходимо писать асинхронный код. If you want the computer to execute the above instructions asynchronously, you must write asynchronous code.
Эти проблемы важны для программ, которые вы пишете уже сегодня. These concerns are important for the programs you write today. При написании клиентских программ требуется, чтобы пользовательский интерфейс реагировал на ввод данных пользователем. When you write client programs, you want the UI to be responsive to user input. Приложения не должны блокировать телефон при скачивании данных из Интернета. Your application shouldn’t make a phone appear frozen while it’s downloading data from the web. При написании серверных программ не стоит блокировать потоки. When you write server programs, you don’t want threads blocked. Эти потоки могут обслуживать другие запросы. Those threads could be serving other requests. Использование синхронного кода в ситуации, когда существуют асинхронные альтернативы, мешает масштабированию с минимальными затратами. Using synchronous code when asynchronous alternatives exist hurts your ability to scale out less expensively. Вы платите за эти заблокированные потоки. You pay for those blocked threads.
Успешные современные приложения требуют использования асинхронного кода. Successful modern applications require asynchronous code. Без поддержки языком при написании асинхронного кода требуются обратные вызовы, события завершения или другие способы, заслоняющие исходное назначение кода. Without language support, writing asynchronous code required callbacks, completion events, or other means that obscured the original intent of the code. Синхронный код удобен тем, что он довольно прост. The advantage of the synchronous code is that it’s easy to understand. Пошаговые действия упрощают чтение и понимание. The step-by-step actions make it easy to scan and understand. Традиционные асинхронные модели заставляют сосредоточиваться на асинхронности кода, а не на фундаментальных действиях в нем. Traditional asynchronous models forced you to focus on the asynchronous nature of the code, not on the fundamental actions of the code.
Не блокировать, а использовать await Don’t block, await instead
Приведенный выше код демонстрирует дурную практику: использование синхронного кода для выполнения асинхронных операций. The preceding code demonstrates a bad practice: constructing synchronous code to perform asynchronous operations. В таком виде код блокирует выполняющий поток, не позволяя делать другие действия. As written, this code blocks the thread executing it from doing any other work. Он не будет прерван, пока задачи выполняются. It won’t be interrupted while any of the tasks are in progress. Все равно что стоять и смотреть на тостер, пока поджаривается хлеб. It would be as though you stared at the toaster after putting the bread in. Пока тост не готов, вы всех игнорируете. You’d ignore anyone talking to you until the toast popped.
Давайте начнем менять этот код, чтобы не блокировать поток во время выполнения задачи. Let’s start by updating this code so that the thread doesn’t block while tasks are running. Ключевое слово await позволяет обойтись без блокировки для запуска задачи, а затем продолжить выполнение, когда задача завершается. The await keyword provides a non-blocking way to start a task, then continue execution when that task completes. Простая асинхронная версия кода для приготовления завтрака будет выглядеть так: A simple asynchronous version of the make a breakfast code would look like the following snippet:
Этот код не блокируется при приготовлении яиц или бекона. This code doesn’t block while the eggs or the bacon are cooking. Этот код, однако, не запускает других задач. This code won’t start any other tasks though. По-прежнему придется поместить тост в тостер и смотреть на него, пока он не выскочит. You’d still put the toast in the toaster and stare at it until it pops. Но по крайней мере можно отвечать всем, кто хочет вашего внимания. But at least, you’d respond to anyone that wanted your attention. В ресторане, где будет размещаться несколько заказов, повар сможет начать готовить другой завтрак, пока первый готовится. In a restaurant where multiple orders are placed, the cook could start another breakfast while the first is cooking.
Теперь поток завтрака не блокируется в ожидании любой запущенной задачи, которая еще не завершена. Now, the thread working on the breakfast isn’t blocked while awaiting any started task that hasn’t yet finished. Для некоторых приложений это изменение — все, что требуется. For some applications, this change is all that’s needed. Приложение с графическим интерфейсом будет отвечать пользователю после этого изменения. A GUI application still responds to the user with just this change. Тем не менее в этом сценарии нам нужно больше. However, for this scenario, you want more. Нам не требуется последовательное выполнение каждой из задач компонента. You don’t want each of the component tasks to be executed sequentially. Лучше запускать каждую из задач компонента, не ожидая завершения предыдущей задачи. It’s better to start each of the component tasks before awaiting the previous task’s completion.
Одновременный запуск задач Start tasks concurrently
Во многих случаях требуется запускать сразу несколько независимых задач. In many scenarios, you want to start several independent tasks immediately. Затем, когда каждая задача завершается, можно продолжить другую работу, которая уже готова к этому. Then, as each task finishes, you can continue other work that’s ready. В нашей аналогии — так завтрак готовится быстрее. In the breakfast analogy, that’s how you get breakfast done more quickly. Вы также приготовите все примерно в одно и то же время. You also get everything done close to the same time. Вы получите горячий завтрак. You’ll get a hot breakfast.
System.Threading.Tasks.Task и связанные типы — это классы, позволяющие делать выводы о задачах, которые находятся в процессе выполнения. The System.Threading.Tasks.Task and related types are classes you can use to reason about tasks that are in progress. Это позволяет писать код, который точнее определяет, как будет фактически готовиться завтрак. That enables you to write code that more closely resembles the way you’d actually create breakfast. Вы начинаете готовить яйца, бекон и тосты примерно в одно и то же время. You’d start cooking the eggs, bacon, and toast at the same time. По мере необходимости вы обращаете внимание на отдельные задачи, переходите к другим, а затем ждете третьих, которые нуждаются в обработке. As each requires action, you’d turn your attention to that task, take care of the next action, then await for something else that requires your attention.
Вы начинаете задачу и удерживаете объект Task, представляющий работу. You start a task and hold on to the Task object that represents the work. Вы вызываете await для каждой задачи, прежде чем начать работу с ее результатами. You’ll await each task before working with its result.
Давайте внесем эти изменения в код для приготовления завтрака. Let’s make these changes to the breakfast code. Первым делом сохраним задачи для отдельных операций при их запуске, чтобы не ждать их: The first step is to store the tasks for operations when they start, rather than awaiting them:
Затем вы можете переместить инструкции await для бекона и яиц в конец метода, сразу перед подачей завтрака: Next, you can move the await statements for the bacon and eggs to the end of the method, before serving breakfast:
Предыдущий код работает лучше. The preceding code works better. Запуск всех асинхронных задач выполняется за один раз. You start all the asynchronous tasks at once. Вы ожидаете каждую задачу только в том случае, когда требуются результаты. You await each task only when you need the results. Приведенный выше код может быть похож на код в веб-приложении, который отправляет запросы для разных микрослужб, а затем объединяет результаты в одну страницу. The preceding code may be similar to code in a web application that makes requests of different microservices, then combines the results into a single page. Вы отправляете все запросы сразу, а затем вызываете await , чтобы соединить все задачи и создать веб-страницу. You’ll make all the requests immediately, then await all those tasks and compose the web page.
Сочетаемость задач Composition with tasks
У вас все готово для завтрака в одно и то же время, за исключением тостов. You have everything ready for breakfast at the same time except the toast. Приготовление тоста — композиция асинхронной операции (поджарить хлеб) и синхронной операции (добавить масло и джем). Making the toast is the composition of an asynchronous operation (toasting the bread), and synchronous operations (adding the butter and the jam). Обновление этого кода иллюстрирует важную концепцию: Updating this code illustrates an important concept:
композиция асинхронной операции, за которой следует синхронная задача, является асинхронной операцией. The composition of an asynchronous operation followed by synchronous work is an asynchronous operation. Говоря иначе, если какая-либо часть операции является асинхронной, то и вся операция является асинхронной. Stated another way, if any portion of an operation is asynchronous, the entire operation is asynchronous.
Предыдущий метод имеет async модификатор в сигнатуре. The preceding method has the async modifier in its signature. Он сообщает компилятору, что этот метод содержит инструкцию await ; она содержит асинхронные операции. That signals to the compiler that this method contains an await statement; it contains asynchronous operations. Этот метод представляет задачу, в рамках которой поджаривается хлеб, а затем добавляется масло и джем. This method represents the task that toasts the bread, then adds butter and jam. Этот метод возвращает Task , представляющий сочетание этих трех операций. This method returns a Task that represents the composition of those three operations. Теперь вид основного блока кода будет таким: The main block of code now becomes:
Предыдущее изменение показывает важную методику для работы с асинхронным кодом. The previous change illustrated an important technique for working with asynchronous code. Составные задачи можно создавать, разделяя операции в новом методе, который возвращает задачу. You compose tasks by separating the operations into a new method that returns a task. Вы можете выбрать, когда следует ожидать выполнения созданной задачи. You can choose when to await that task. Одновременно можно запускать другие задачи. You can start other tasks concurrently.
Эффективное ожидание задач Await tasks efficiently
Ряд инструкций await в конце приведенного выше кода можно улучшить с помощью методов класса Task . The series of await statements at the end of the preceding code can be improved by using methods of the Task class. Один из этих API — WhenAll, который возвращает Task; она завершается после завершения всех задач в списке аргументов, как показано в следующем коде: One of those APIs is WhenAll, which returns a Task that completes when all the tasks in its argument list have completed, as shown in the following code:
Другой вариант — использовать WhenAny, который возвращает Task , выполняемый по завершении любого из своих аргументов. Another option is to use WhenAny, which returns a Task that completes when any of its arguments completes. Можно ожидать возвращенной задачи, зная, что она уже завершена. You can await the returned task, knowing that it has already finished. В следующем коде показано, как использовать WhenAny для ожидания первой задачи, чтобы затем обработать ее результат. The following code shows how you could use WhenAny to await the first task to finish and then process its result. После обработки результата завершенной задачи удалим ее из списка задач, передаваемого в WhenAny . After processing the result from the completed task, you remove that completed task from the list of tasks passed to WhenAny .
После всех этих изменений окончательная версия Main выглядит так: After all those changes, the final version of Main looks like the following code:
Этот итоговый код выполняется асинхронно. This final code is asynchronous. Он более точно отражает, как пользователь будет готовить завтрак. It more accurately reflects how a person would cook a breakfast. Сравните предыдущий код с первым примером кода в этой статье. Compare the preceding code with the first code sample in this article. Основные действия по-прежнему очевидны при прочтении. The core actions are still clear from reading the code. Этот код можно прочитать так же, как указания по приготовлению завтрака в начале этой статьи. You can read this code the same way you’d read those instructions for making a breakfast at the beginning of this article. Возможности языка для async и await делают возможными преобразования, которые любой человек производит, выполняя эти инструкции: запуск задач без блокировки в ожидании их завершения. The language features for async and await provide the translation every person makes to follow those written instructions: start tasks as you can and don’t block waiting for tasks to complete.
Это краткая выжимка из рабочего доклада по работе с async/await в C#. Для наглядности, параллельно рассматривается подход с использованием блока ContinueWith .
Основные паттерны асинхронного программирования
- Asynchronous Programming Model (APM)
- Event Asynchronous Pattern (EAP)
- Task Asynchronous Pattern (TAP)
- Задачи представляют параллельные операции
- Могут выполняться на отдельном или разных потоках.
- Могут быть скомбинированы и выстроены в цепочку вызовов.
async/await — синтаксическая обертка над задачами
- await ставит на паузу текущий метод, ожидая выполнения задачи.
- Выглядит как блокирующая (синхронная) операция.
- Не блокирует текущий поток.
- Выполнение продолжается в том же контексте, из которого была вызвана задача, если явно не указано иное.
- Ключевое слово async указывается, чтобы среда исполнения воспринимала await как ключевое слово.
- await метод начинает выполняться синхронно. Если он уже закончил свое выполнение то новый поток не создается. Все продолжается в том же потоке. Подробнее в ответе на stackoverflow.com.
- await работает с любым типом, для которого реализован метод GetAwaiter() . Подробнее в статье — await anything.
Демонстрационное приложение
Примеры показываются на тестовом Windows Form приложении. GitHub репозиторий с приложением.

Асинхронные действия лежат в PeopleRepositoryAsync :
Метод GetPeopleList() асинхронно ожидает 2 секунды и возвращает список пользователей.
Первое сравнение TAP и async/await подхода
Реализация с Task и ContinueWith
Добавим код для получения списка пользователей в обработчик нажатия кнопки “Fetch Data (with Task)” — buttonTask_Click :
Этот код не будет выполняться асинхронно. Он будет ожидать завершение задачи peopleTask в основном потоке, поэтому UI заморозится. Добавим конструкцию ContinueWith(t => < >) :
Теперь задача по получению пользователей выполнится асинхронно. После её завершения выполнится код в блоке ContinueWith . Добавим в ContinueWith отображение полученных имен в textBoxMain :
Если запустить приложение и нажать на “Fetch Data (with Task)”, то возникнет ошибка. Все потому, что код в блоке ContinueWith выполняется в потоке, отличном от того где находится SynchronizationContext UI потока. Для выполнения в нужном потоке добавим в вызов метода ContinueWith аргумент TaskScheduler.FromCurrentSynchronizationContext() :
Теперь приложение работает корректно. Перейдем к реализации этого кода с помощью ключевых слов async и await .
Реализация с async/await
Основное отличие от предыдущей реализации — код будет похож на синхронный. Перейдем в обработчик нажатия кнопки “Fetch Data (with await)” — buttonAwait_Click . Добавим код для получения списка пользователей:
В объявление метода добавим слово async , чтобы среда исполнения поняла что await это ключевое слово, а не просто переменная. Вставим без изменений код из ContinueWith :
Все. Приложение работает так же как и в предыдущем пункте. Код выглядит как синхронный. Выполнение продолжается в том же потоке, из которого и было вызвано.
Обработка ошибок
Для демонстрации добавим в метод GetPeopleList код вызова ошибки (после Task.Delay(2000) ):
Обработка ошибок для await метода
Сначала рассмотрим самый простой случай. Отлов и обработка ошибок для метода с await происходит как в синхронном коде:
Всё. Дополнительно писать ничего не надо, ошибка будет поймана.
Обработка ошибок для Task метода
Для метода с блоком ContinueWith обработать ошибки можно несколькими способами.
Первый, использовать еще один вызов ContinueWith на задаче. Вызовем ContinueWith с 2 дополнительными параметрами:
Главное — аргумент TaskContinuationOptions.OnlyOnRanToCompletion . Он указывает, что блок кода выполнится только если в задаче не было ошибок.
Теперь, код для обработки ошибки:
Опция OnlyOnFaulted указывает, что код в блоке выполниться только при ошибке в задаче. Оператор foreach разворачивает ошибки в «плоское» состояние, т. к. все ошибки представляются в виде иерархии и оборачиваются в AggregateException .
Для имитации блока finally , напишем:
Второй способ обработки ошибок занимает меньше строк. В блок ContinueWith добавляется условный оператор, который проверяет статус задачи:
Отмена действий
Обновим метод GetPeopleList класса Repository . Добавим параметр CancellationToken и точку отмены после вызова Task.Delay(2000) :
Обратит внимание, что отмена произойдет только после ожидания в 2 секунды. Само действие Task.Delay не прерывается.
Для операции отмены используем кнопку “Cancel request” с обработчиком buttonCancel_Click . Объекту CancellationToken можно задать значение только при инициализации. Поэтому создадим переменную CancellationTokenSource . Она позволяет генерировать токены и изменять их состояние во время выполнения.
В класс MainForm добавим поле:
А в обработчик buttonCancel_Click код для подачи токену сигнала отмены:
Обработка отмены для async метода
В обработчике buttonAwait_Click изменим вызов метода GetPeopleList() , добавив инициализацию _tokenSource и передав сгенерированный токен в качестве аргумента:
Добавим обработку операции отмены:
Обработка отмены для Task метода
Для buttonTask_Click добавим похожий код для передачи токена:
Для обработки операции отмены в блок ContinueWith добавим:
Deadlocks
Добавим в класс Repository метод DeadlockTestAsync() :
Вызовем этот метод в обработчике кнопки “Deadlock”:
Все. При нажатии на кнопку возникнет Deadlock. Почему? Рассмотрим по пунктам:
- DeadlockTestAsync() вызывается на потоке с UI.
- Task.Delay() запускается в новом потоке.
- await захватывает SynchronizationContext и подключает continuation для выполнения действий после завершения.
- Вернемся к вызову DeadlockTestAsync() .
- Wait() ждет завершение задачи в UI потоке.
- Task.Delay() ожидает выполнить продолжение на UI потоке.
- Но поток в ожидание — Дедлок!
- Все потому, что задача не вернется из DeadlockTestAsync() , пока не выполнится “продолжение”.
Для избежания подобной ситуации, в библиотеках, лучше писать .ConfigureAwait(false) :
Это позволит выполнить “продолжение” в том же потоке, в котором работала задача. В моем примере это будет поток, отличный от UI потока.





