День 1253. #ЗаметкиНаПолях #AsyncTips
Регулировка очередей
Задача
Имеется очередь «производитель/потребитель», но производители могут работать быстрее потребителей, что может привести к неэффективному использованию памяти. Также вам хотелось бы сохранить все элементы в очереди, а следовательно, требуется механизм регулировки производителей.
Решение
Если элементы производятся быстрее, чем потребители могут потреблять их, очередь придётся отрегулировать. Для этого можно задать максимальное количество элементов. Когда очередь будет «заполнена», она блокирует производителей, пока в очереди не появится свободное место.
Регулировка может выполняться посредством создания ограниченного канала (вместо неограниченного). Так как каналы асинхронны, производители будут регулироваться асинхронно:
Блокирующие очереди «производитель/потребитель» также поддерживают регулировку. Вы можете использовать тип
Регулировка необходима в том случае, если производители работают быстрее потребителей. Один из сценариев, которые необходимо рассмотреть: могут ли производители работать быстрее потребителей, если ваше приложение будет работать на другом оборудовании? Обычно некоторая регулировка потребуется для того, чтобы гарантировать нормальную работу на будущем оборудовании и/или облачных платформах, которые нередко более ограничены в ресурсах, чем машины разработчиков.
Регулировка замедляет работу производителей, блокируя их, чтобы потребители гарантированно могли обработать все элементы без излишних затрат памяти. Если обрабатывать каждый элемент не обязательно, можно использовать выборку вместо регулировки (об этом в будущих постах).
См. также
- Асинхронные очереди
- Блокирующие очереди
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
Регулировка очередей
Задача
Имеется очередь «производитель/потребитель», но производители могут работать быстрее потребителей, что может привести к неэффективному использованию памяти. Также вам хотелось бы сохранить все элементы в очереди, а следовательно, требуется механизм регулировки производителей.
Решение
Если элементы производятся быстрее, чем потребители могут потреблять их, очередь придётся отрегулировать. Для этого можно задать максимальное количество элементов. Когда очередь будет «заполнена», она блокирует производителей, пока в очереди не появится свободное место.
Регулировка может выполняться посредством создания ограниченного канала (вместо неограниченного). Так как каналы асинхронны, производители будут регулироваться асинхронно:
var queue = Channel.CreateBounded<int>(1);Тип
var writer = queue.Writer;
// Эта запись завершается немедленно.
await writer.WriteAsync(7);
// Эта запись (асинхронно) ожидает удаления 7
// перед тем как вставить в очередь 13.
await writer.WriteAsync(13);
writer.Complete();
BufferBlock<T>
также имеет встроенную поддержку регулировки, следует задать параметр BoundedCapacity
:var queue = new BufferBlock<int>(Производитель в этом фрагменте кода использует асинхронный метод
new DataflowBlockOptions
{
BoundedCapacity = 1
});
// Эта отправка завершается немедленно.
await queue.SendAsync(7);
// Эта отправка (асинхронно) ожидает удаления 7
// перед тем как ставить в очередь 13.
await queue.SendAsync(13);
queue.Complete();
SendAsync
.Блокирующие очереди «производитель/потребитель» также поддерживают регулировку. Вы можете использовать тип
BlockingCollection<T>
для регулировки количества элементов, для чего при создании передается соответствующее значение:var queue = new BlockingCollection<int>(boundedCapacity: 1);Итого
// Это добавление завершается немедленно.
queue.Add(7);
// Это добавление ожидает удаления 7
// перед тем, как добавлять 13.
queue.Add(13);
queue.CompleteAdding();
Регулировка необходима в том случае, если производители работают быстрее потребителей. Один из сценариев, которые необходимо рассмотреть: могут ли производители работать быстрее потребителей, если ваше приложение будет работать на другом оборудовании? Обычно некоторая регулировка потребуется для того, чтобы гарантировать нормальную работу на будущем оборудовании и/или облачных платформах, которые нередко более ограничены в ресурсах, чем машины разработчиков.
Регулировка замедляет работу производителей, блокируя их, чтобы потребители гарантированно могли обработать все элементы без излишних затрат памяти. Если обрабатывать каждый элемент не обязательно, можно использовать выборку вместо регулировки (об этом в будущих постах).
См. также
- Асинхронные очереди
- Блокирующие очереди
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
👍3
День 1267. #ЗаметкиНаПолях #AsyncTips
Выборка в очередях
Задача
Есть очередь «производитель/потребитель», но производители могут работать быстрее потребителей, что может привести к неэффективному использованию памяти. Сохранять все элементы из очереди не обязательно; необходимо отфильтровать элементы очереди так, чтобы более медленные потребители могли ограничиться обработкой самых важных элементов.
Решение
Библиотека
Есть и другие режимы
Библиотека
Если выборка должна выполняться по времени (например, «только 10 элементов в секунду»), используйте
См. также
- Асинхронные очереди
- Блокирующие очереди
- Регулировка очередей
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
Выборка в очередях
Задача
Есть очередь «производитель/потребитель», но производители могут работать быстрее потребителей, что может привести к неэффективному использованию памяти. Сохранять все элементы из очереди не обязательно; необходимо отфильтровать элементы очереди так, чтобы более медленные потребители могли ограничиться обработкой самых важных элементов.
Решение
Библиотека
Channels
предоставляет самые простые средства применения выборки к элементам ввода. Типичный пример — всегда брать последние n элементов с потерей самых старых элементов при заполнении очереди:var queue = Channel.CreateBounded<int>(Это самый простой механизм контроля входных потоков и предотвращения «затопления» потребителей.
new BoundedChannelOptions(1)
{
FullMode = BoundedChannelFullMode.DropOldest,
});
var writer = queue.Writer;
// Операция записи завершается немедленно.
await writer.WriteAsync(7);
// Операция записи тоже завершается немедленно.
// Элемент 7 теряется, если только он не был
// немедленно извлечен потребителем.
await writer.WriteAsync(13);
Есть и другие режимы
BoundedChannelFullMode
. Например, если вы хотите, чтобы самые старые элементы сохранялись, можно при заполнении канала терять новые элементы:var queue = Channel.CreateBounded<int>(Пояснение
new BoundedChannelOptions(1)
{
FullMode = BoundedChannelFullMode.DropWrite,
});
var writer = queue.Writer;
// Операция записи завершается немедленно
await writer.WriteAsync(7);
// Операция записи тоже завершается немедленно
// Элемент 13 теряется, если только элемент 7 не был
// немедленно извлечен потребителем.
await writer.WriteAsync(13);
Библиотека
Channels
отлично подходит для простой выборки. Во многих ситуациях полезен режим BoundedChannelFullMode.DropOldest
. Более сложная выборка должна выполняться самими потребителями.Если выборка должна выполняться по времени (например, «только 10 элементов в секунду»), используйте
System.Reactive
. В System.Reactive
предусмотрены естественные операторы для работы со временем.См. также
- Асинхронные очереди
- Блокирующие очереди
- Регулировка очередей
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
👍5
День 1279. #ЗаметкиНаПолях #AsyncTips
Связанные Запросы на Отмену
Подробнее про токены отмены см. «Скоординированная отмена».
Задача: в коде присутствует промежуточный уровень, который должен реагировать на запросы на отмену «сверху», а также выдавать собственные запросы на отмену на следующий уровень.
Решение
В .NET предусмотрена встроенная поддержка этого сценария в виде связанных токенов отмены. Источник токена отмены может быть создан связанным с одним (или несколькими) существующими токенами. Когда вы создаёте источник связанного токена отмены, полученный токен будет отменяться как при непосредственной отмене его, так и при отмене любого из связанных токенов.
Следующий пример выполняет асинхронный запрос HTTP. Токен
Хотя в примере выше используется только один токен
Например, ASP.NET предоставляет токен отмены, представляющий отключение пользователя. Код обработчика может создать связанный токен, который реагирует либо на отключение пользователя, либо на свои причины отмены (например, тайм-аут).
Помните о сроке существования источника связанного токена отмены. Предыдущий пример является наиболее типичным: один или несколько токенов отмены передаются методу, который связывает их и передаёт как комбинированный токен. Обратите внимание на то, что в примере используется команда using, которая гарантирует, что источник связанного токена отмены будет освобожден, когда операция будет завершена (а комбинированный токен перестанет использоваться). Подумайте, что произойдёт, если код не освободит источник связанного токена отмены: может оказаться, что метод
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
Связанные Запросы на Отмену
Подробнее про токены отмены см. «Скоординированная отмена».
Задача: в коде присутствует промежуточный уровень, который должен реагировать на запросы на отмену «сверху», а также выдавать собственные запросы на отмену на следующий уровень.
Решение
В .NET предусмотрена встроенная поддержка этого сценария в виде связанных токенов отмены. Источник токена отмены может быть создан связанным с одним (или несколькими) существующими токенами. Когда вы создаёте источник связанного токена отмены, полученный токен будет отменяться как при непосредственной отмене его, так и при отмене любого из связанных токенов.
Следующий пример выполняет асинхронный запрос HTTP. Токен
ct
, переданный методу GetWithTimeoutAsync
, представляет отмену, запрошенную конечным пользователем, а сам метод также применяет тайм-аут к запросу:async Task<HttpResponseMessage>Полученный токен
GetWithTimeoutAsync(
HttpClient client,
string url,
CancellationToken ct)
{
using var cts =
CancellationTokenSource
.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(2));
var combinedToken = cts.Token;
return await client.GetAsync(url, combinedToken);
}
combinedToken
отменяется либо когда пользователь отменяет существующий токен ct
, либо при отмене связанного источника вызовом CancelAfter
.Хотя в примере выше используется только один токен
ct
, метод CreateLinkedTokenSource
может получать любое количество токенов отмены в своих параметрах. Это позволяет вам создать один объединённый токен, на базе которого можно реализовать собственную логику отмены. Например, ASP.NET предоставляет токен отмены, представляющий отключение пользователя. Код обработчика может создать связанный токен, который реагирует либо на отключение пользователя, либо на свои причины отмены (например, тайм-аут).
Помните о сроке существования источника связанного токена отмены. Предыдущий пример является наиболее типичным: один или несколько токенов отмены передаются методу, который связывает их и передаёт как комбинированный токен. Обратите внимание на то, что в примере используется команда using, которая гарантирует, что источник связанного токена отмены будет освобожден, когда операция будет завершена (а комбинированный токен перестанет использоваться). Подумайте, что произойдёт, если код не освободит источник связанного токена отмены: может оказаться, что метод
GetWithTimeoutAsync
будет вызван несколько раз с одним (долгосрочным) существующим токеном; в этом случае код будет создавать новый источник связанного токена при каждом вызове. Даже после того, как запросы HTTP завершатся (и ничто не будет использовать комбинированный токен), эти связанные источники будут оставаться присоединенными к существующему токену. Чтобы предотвратить подобные утечки памяти, освобождайте источник связанного токена отмены, когда комбинированный токен перестаёт быть нужным.Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
👍4
День 1286. #ЗаметкиНаПолях #AsyncTips
Реагирование на Запросы на Отмену Посредством Периодического Опроса
Задача: В коде имеется цикл, который должен поддерживать отмену.
Решение
Если в коде присутствует цикл обработки, то в нём нет низкоуровневых функций API, которым можно было передать
В большинстве случаев ваш код должен просто передать
У типа
Работа метода
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
Реагирование на Запросы на Отмену Посредством Периодического Опроса
Задача: В коде имеется цикл, который должен поддерживать отмену.
Решение
Если в коде присутствует цикл обработки, то в нём нет низкоуровневых функций API, которым можно было передать
CancellationToken
. В этом случае необходимо периодически проверять, не был ли отменён токен. Следующий пример периодически проверяет токен в ходе выполнения цикла, создающего вычислительную нагрузку на процессор:public int Cancelable(CancellationToken ct)Если тело цикла выполняется очень быстро, то, возможно, стоит ограничить частоту проверки токена отмены. Измерьте быстродействие до и после таких изменений, чтобы выбрать лучший вариант. Следующий пример похож на предыдущий, но выполняет больше итераций более быстрого цикла, поэтому добавлено ограничение на частоту проверки маркера:
{
for (int i = 0; i != 100; ++i)
{
Thread.Sleep(1000); // вычисления
ct.ThrowIfCancellationRequested();
}
return 42;
}
public int Cancelable(CancellationToken ct)Оптимальная частота опроса зависит исключительно от того, какой объём работы выполняется и насколько быстрой должна быть реакция на отмену.
{
for (int i = 0; i != 100000; ++i)
{
Thread.Sleep(1); // вычисления
if (i % 1000 == 0)
ct.ThrowIfCancellationRequested();
}
return 42;
}
В большинстве случаев ваш код должен просто передать
CancellationToken
на следующий уровень. Метод периодического опроса (polling), использованный в этом рецепте, следует применять только в том случае, если у вас имеется вычислительный цикл, который должен поддерживать отмену.У типа
CancellationToken
имеется другой метод IsCancellationRequested
, который начинает возвращать true при отмене токена. Некоторые разработчики используют его для реакции на отмену, обычно возвращая значение по умолчанию или null. В большинстве случаев использовать этот метод не рекомендуется. В стандартном паттерне отмены выдаётся исключение OperationCanceledException
, для чего вызывается метод ThrowIfCancellationRequested
. Если код, находящийся выше в стеке, захочет перехватить исключение и действовать так, словно результат равен null, это нормально, но любой код, получающий CancellationToken
, должен следовать стандартному паттерну отмены. Если вы решите не соблюдать паттерн отмены, по крайней мере чётко документируйте свои намерения.Работа метода
ThrowIfCancellationRequested
основана на периодическом опросе токена отмены; ваш код должен вызывать его с регулярными интервалами. Также существует способ регистрации обратного вызова, который вызывается при запросе на отмену. Об этом в будущих постах.Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
👍4
День 1294. #ЗаметкиНаПолях #AsyncTips
Отмена по тайм-ауту
Тайм-аут — всего лишь одна из разновидностей запроса на отмену. Код, который необходимо отменить, просто отслеживает токен отмены, как и при любой другой отмене; ему не нужно знать, что источником отмены является таймер. У источников токенов отмены существуют вспомогательные методы, которые автоматически выдают запрос на отмену по тайм-ауту:
Многие асинхронные API поддерживают CancellationToken, поэтому обеспечение отмены обычно сводится к простой передаче токена. Как правило, если ваш метод вызывает функции API, получающие CancellationToken, то ваш метод также должен получать CancellationToken и передавать его всем функциям API, которые его поддерживают.
К сожалению, некоторые методы не поддерживают отмену. В такой ситуации простого решения не существует. Невозможно безопасно остановить произвольный код, если только он не упакован в отдельный исполняемый модуль. Если ваш код вызывает код, не поддерживающий отмену и вы не хотите упаковывать этот код в отдельный исполняемый модуль, всегда можно имитировать отмену, просто игнорируя результат.
Отмена должна предоставляться как вариант там, где это возможно. Дело в том, что правильно реализованная отмена на высоком уровне зависит от правильно реализованной отмены на нижнем уровне. Таким образом, когда вы пишете собственные async-методы, постарайтесь как можно тщательнее обеспечить поддержку отмены. Никогда неизвестно заранее, какие высокоуровневые методы будут вызывать ваш код, и им тоже может понадобиться отмена.
Отмена параллельного кода
Простейший способ поддержки отмены — передача CancellationToken параллельному коду через ParallelOptions:
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
Отмена по тайм-ауту
Тайм-аут — всего лишь одна из разновидностей запроса на отмену. Код, который необходимо отменить, просто отслеживает токен отмены, как и при любой другой отмене; ему не нужно знать, что источником отмены является таймер. У источников токенов отмены существуют вспомогательные методы, которые автоматически выдают запрос на отмену по тайм-ауту:
using var cts = new CancellationTokenSource();Кроме того, тайм-аут можно передать конструктору:
cts.CancelAfter(TimeSpan.FromSeconds(5));
async Task IssueTimeoutAsync()Отмена async-кода
{
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(5));
var token = cts.Token;
await Task.Delay(TimeSpan.FromSeconds(10), token);
}
Многие асинхронные API поддерживают CancellationToken, поэтому обеспечение отмены обычно сводится к простой передаче токена. Как правило, если ваш метод вызывает функции API, получающие CancellationToken, то ваш метод также должен получать CancellationToken и передавать его всем функциям API, которые его поддерживают.
К сожалению, некоторые методы не поддерживают отмену. В такой ситуации простого решения не существует. Невозможно безопасно остановить произвольный код, если только он не упакован в отдельный исполняемый модуль. Если ваш код вызывает код, не поддерживающий отмену и вы не хотите упаковывать этот код в отдельный исполняемый модуль, всегда можно имитировать отмену, просто игнорируя результат.
Отмена должна предоставляться как вариант там, где это возможно. Дело в том, что правильно реализованная отмена на высоком уровне зависит от правильно реализованной отмены на нижнем уровне. Таким образом, когда вы пишете собственные async-методы, постарайтесь как можно тщательнее обеспечить поддержку отмены. Никогда неизвестно заранее, какие высокоуровневые методы будут вызывать ваш код, и им тоже может понадобиться отмена.
Отмена параллельного кода
Простейший способ поддержки отмены — передача CancellationToken параллельному коду через ParallelOptions:
void Rotate(В Parallel LINQ (PLINQ) также предусмотрена встроенная поддержка отмены с оператором WithCancellation:
IEnumerable<Matrix> matrices,
float degrees,
CancellationToken ct)
{
Parallel.ForEach(matrices,
new ParallelOptions { CancellationToken = ct },
m => m.Rotate(degrees));
}
IEnumerable<int> MultiplyBy2(Поддержка отмены для параллельной работы — важный критерий хорошего пользовательского интерфейса. Если ваше приложение выполняет параллельную работу, оно создает серьезную нагрузку на процессор пусть даже на короткое время. Высокий уровень использования процессора обычно заметен для пользователей, даже если не мешает работе других приложений на той же машине.
IEnumerable<int> values,
CancellationToken ct)
{
return values.AsParallel()
.WithCancellation(ct)
.Select(item => item * 2);
}
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
👍10
День 1301. #ЗаметкиНаПолях #AsyncTips
Внедрение Запросов на Отмену
Задача: В коде присутствует уровень, который должен реагировать на запросы на отмену, а также выдавать собственные запросы на отмену на следующий уровень.
Решение
В системе отмены .NET предусмотрена встроенная поддержка этого сценария в виде связанных токенов отмены. Источник токена отмены может быть создан связанным с одним (или несколькими) существующими токенами. Когда вы создаёте источник связанного токена отмены, полученный токен будет отменяться при отмене любых из существующих токенов или при явной отмене связанного источника.
Следующий пример выполняет асинхронный запрос HTTP. Токен, переданный методу
Хотя в предыдущем примере используется только один источник
Помните о сроке существования источника связанного токена отмены. Предыдущий пример является наиболее типичным: один или несколько токенов отмены передаются методу, который связывает их и передает как комбинированный токен. Также обратите внимание на то, что в примере используется команда
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
Внедрение Запросов на Отмену
Задача: В коде присутствует уровень, который должен реагировать на запросы на отмену, а также выдавать собственные запросы на отмену на следующий уровень.
Решение
В системе отмены .NET предусмотрена встроенная поддержка этого сценария в виде связанных токенов отмены. Источник токена отмены может быть создан связанным с одним (или несколькими) существующими токенами. Когда вы создаёте источник связанного токена отмены, полученный токен будет отменяться при отмене любых из существующих токенов или при явной отмене связанного источника.
Следующий пример выполняет асинхронный запрос HTTP. Токен, переданный методу
GetWithTimeoutAsync
, представляет отмену, запрошенную конечным пользователем, а метод GetWithTimeoutAsync
также применяет тайм-аут к запросу:async Task<HttpResponseMessage>Полученный токен
GetWithTimeoutAsync(
HttpClient client,
string url,
CancellationToken ct)
{
using var cts = CancellationTokenSource
.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(2));
var combined = cts.Token;
return await client.GetAsync(url, combined);
}
combined
отменяется либо когда пользователь отменяет существующий маркер ct
, либо при отмене связанного источника вызовом CancelAfter
.Хотя в предыдущем примере используется только один источник
CancellationToken
, метод CreateLinkedTokenSource
может получать любое количество токенов отмены в своих параметрах. Это позволяет создавать один объединённый токен, на базе которого можно реализовать собственную логическую отмену. Например, ASP.NET предоставляет токен отмены, представляющий отключение пользователя (HttpContext.RequestAborted); код обработчика может создать связанный токен, который реагирует либо на отключение пользователя, либо на свои причины отмены (например, тайм-аут).Помните о сроке существования источника связанного токена отмены. Предыдущий пример является наиболее типичным: один или несколько токенов отмены передаются методу, который связывает их и передает как комбинированный токен. Также обратите внимание на то, что в примере используется команда
using
, которая гарантирует, что источник связанного токена отмены будет освобожден, когда операция будет завершена (а комбинированный токен перестанет использоваться). Подумайте, что произойдет, если код не освободит источник связанного токена отмены: может оказаться, что метод GetWithTimeoutAsync
будет вызван несколько раз с одним (долгосрочным) существующим токеном; в этом случае код будет связывать новый источник токена при каждом вызове метода. Даже после того, как запросы HTTP завершатся (и ничто не будет использовать комбинированный токен), этот связанный источник всё ещё останется присоединённым к существующему токену. Чтобы предотвратить подобные утечки памяти, освобождайте источник связанного токена отмены, когда комбинированный токен перестаёт быть нужным.Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
👍8
День 1311. #ЗаметкиНаПолях #AsyncTips
Взаимодействие с другими системами отмены
Задача
Имеется внешний или унаследованный код с собственными концепциями отмены. Требуется управлять им с использованием стандартного объекта
Решение
У типа
Допустим, вы пишете обертку для
Метод
Помните о сроке существования регистрации обратных вызовов. Метод
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
Взаимодействие с другими системами отмены
Задача
Имеется внешний или унаследованный код с собственными концепциями отмены. Требуется управлять им с использованием стандартного объекта
CancellationToken
.Решение
У типа
CancellationToken
существует два основных способа реакции на запрос на отмену: периодический опрос и обратные вызовы. Периодический опрос обычно используется для кода, интенсивно использующего процессор, — например, циклов обработки данных; обратные вызовы обычно используются во всех остальных ситуациях. Регистрация обратного вызова для маркера осуществляется методом CancellationToken.Register
.Допустим, вы пишете обертку для
System.Net.NetworkInformation.Pingtype
и хотите предусмотреть возможность отмены тестового опроса. Класс Ping
уже имеет API на базе Task
, но не поддерживает CancellationToken
. Вместо этого тип Ping
содержит собственный метод SendAsyncCancel
, который может использоваться для отмены. Для этого зарегистрируйте обратный вызов, который активизирует этот метод:async Task<PingReply> PingAsync(
string host,
CancellationToken ct)
{
using var ping = new Ping();
var task = ping.SendPingAsync(host);
using CancellationTokenRegistration _ =
ct.Register(() => ping.SendAsyncCancel());
return await task;
}
Теперь при запросе на отмену CancellationToken
вызовет метод SendAsyncCancel
за вас, отменяя метод SendPingAsync
.Метод
CancellationToken.Register
может использоваться для взаимодействия с любой альтернативной системой отмены. Но следует помнить, что, если метод получает CancellationToken
, запрос отмены должен отменять только эту одну операцию. Некоторые альтернативные системы отмены реализуют отмену закрытием некоторого ресурса, что может привести к отмене нескольких операций; эта разновидность системы отмены плохо соответствует CancellationToken
. Если вы решите инкапсулировать такую разновидность отмены в CancellationToken
, следует документировать её необычную семантику отмены.Помните о сроке существования регистрации обратных вызовов. Метод
Register
возвращает отменяемый объект, который должен быть освобожден, когда обратный вызов перестанет быть нужным. Предыдущий пример использует команду using для выполнения очистки при завершении асинхронной операции. Если в коде отсутствует команда using, то при каждом вызове кода с тем же (долгосрочным) маркером CancellationToken
он будет добавлять новый обратный вызов (который, в свою очередь, будет поддерживать существование объекта Ping
). Чтобы избежать утечки памяти и ресурсов, очищайте регистрацию обратного вызова, когда он перестаёт быть нужным.Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 10.
👍8
День 1320. #ЗаметкиНаПолях #AsyncTips
Асинхронные интерфейсы и наследование
Задача: В интерфейсе или базовом классе имеется метод, который требуется сделать асинхронным.
Решение
Ключом к пониманию этой задачи и её решения станет понимание того, что async относится к подробностям реализации. Ключевое слово async может применяться только к методам с реализациями. Невозможно применить его к абстрактным методам или методам интерфейсов (если они не имеют реализации по умолчанию). Тем не менее вы можете определить метод с такой же сигнатурой, как у async-метода, но без ключевого слова async.
Ожидание допускают типы, а не методы. Вы можете использовать await с объектом Task, возвращённым методом, независимо от того, был метод реализован с ключевым словом async или нет. Таким образом, интерфейс или абстрактный метод может просто вернуть Task (или Task<T>), и это значение может ожидаться.
Следующий пример определяет интерфейс с асинхронным методом (без ключевого слова async), реализацию этого интерфейса (с async), и независимый метод, который потребляет метод этого интерфейса (посредством await):
Асинхронная сигнатура метода (возврат Task или Task<T>) означает лишь то, что реализация может быть асинхронной. Фактическая реализация может быть синхронной, если нет реальной асинхронной работы, которую нужно было бы выполнять. Например, тестовая заглушка может реализовать тот же интерфейс (без async), используя, например,
См. также: Возвращение завершённых задач
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
Асинхронные интерфейсы и наследование
Задача: В интерфейсе или базовом классе имеется метод, который требуется сделать асинхронным.
Решение
Ключом к пониманию этой задачи и её решения станет понимание того, что async относится к подробностям реализации. Ключевое слово async может применяться только к методам с реализациями. Невозможно применить его к абстрактным методам или методам интерфейсов (если они не имеют реализации по умолчанию). Тем не менее вы можете определить метод с такой же сигнатурой, как у async-метода, но без ключевого слова async.
Ожидание допускают типы, а не методы. Вы можете использовать await с объектом Task, возвращённым методом, независимо от того, был метод реализован с ключевым словом async или нет. Таким образом, интерфейс или абстрактный метод может просто вернуть Task (или Task<T>), и это значение может ожидаться.
Следующий пример определяет интерфейс с асинхронным методом (без ключевого слова async), реализацию этого интерфейса (с async), и независимый метод, который потребляет метод этого интерфейса (посредством await):
interface ICountBytes…
{
Task<int> CountBytesAsync(
HttpClient hc, string url);
}
class Counter : ICountBytes
{
public async Task<int>
CountBytesAsync(HttpClient hc, string url)
{
var b = await hc.GetByteArrayAsync(url);
return b.Length;
}
}
async Task UseCounter(Этот паттерн работает и с абстрактными методами базовых классов.
HttpClient hc,
ICountBytes svc)
{
var res = await svc.CountBytesAsync(
hc,
"http://www.google.com");
Console.WriteLine(res);
}
Асинхронная сигнатура метода (возврат Task или Task<T>) означает лишь то, что реализация может быть асинхронной. Фактическая реализация может быть синхронной, если нет реальной асинхронной работы, которую нужно было бы выполнять. Например, тестовая заглушка может реализовать тот же интерфейс (без async), используя, например,
FromResult
:class CounterStub : ICountBytesАсинхронные методы интерфейсов и базовых классов встречаются всё чаще. Работать с ними не так уж сложно, если помнить, что ожидание должно применяться к возвращаемому типу (а не к методу), а определение асинхронного метода может быть реализовано как асинхронно, так и синхронно.
{
public Task<int> CountBytesAsync(
HttpClient hc, string url)
{
return Task.FromResult(13);
}
}
См. также: Возвращение завершённых задач
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
👍7
День 1328. #ЗаметкиНаПолях #AsyncTips
Асинхронное Создание Объектов: Фабрики
Задача: вы создаёте тип, который требует выполнения некоторой асинхронной работы в конструкторе.
Решение
Конструкторы не могут объявляться с async; кроме того, они не могут содержать ключевое слово await. Конечно, использование await в конструкторе могло бы быть полезным, но это привело бы к существенному изменению языка C#.
Одна из возможностей — использовать конструктор в паре с инициализирующим async-методом, чтобы тип использовался следующим образом:
Вот более качественное решение, которое основано на применении паттерна асинхронного фабричного метода:
Экземпляр может создаваться следующим образом:
К сожалению, в некоторых сценариях этот способ не работает — в частности, когда в коде используется провайдер внедрения зависимостей. Ни одна заметная библиотека внедрения зависимостей или инверсии управления не работает с async-кодом. Если вы окажетесь в одной из таких ситуаций, существует пара альтернатив, которые также стоит рассмотреть.
Если создаваемый экземпляр в действительности является общим ресурсом, можно использовать ленивую инициализацию. В противном случае можно воспользоваться паттерном асинхронной инициализации, который рассмотрим позже.
Пример того, как поступать не следует:
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
Асинхронное Создание Объектов: Фабрики
Задача: вы создаёте тип, который требует выполнения некоторой асинхронной работы в конструкторе.
Решение
Конструкторы не могут объявляться с async; кроме того, они не могут содержать ключевое слово await. Конечно, использование await в конструкторе могло бы быть полезным, но это привело бы к существенному изменению языка C#.
Одна из возможностей — использовать конструктор в паре с инициализирующим async-методом, чтобы тип использовался следующим образом:
var instance = new MyAsyncClass();У такого подхода есть недостатки. Разработчик может забыть вызвать метод
await instance.InitAsync();
InitAsync
, а экземпляр не может использоваться сразу же после выполнения конструктора.Вот более качественное решение, которое основано на применении паттерна асинхронного фабричного метода:
class AsyncClassКонструктор и метод
{
private AsyncClass()
{
}
private async Task<AsyncClass> InitAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
return this;
}
public static Task<AsyncClass> CreateAsync()
{
var result = new AsyncClass();
return result.InitAsync();
}
}
InitAsync
объявлены приватными, чтобы они не могли использоваться в вызывающем коде; экземпляры могут создаваться только одним способом — статическим фабричным методом CreateAsync
. Вызывающий код не может обратиться к экземпляру до того, как инициализация будет завершена.Экземпляр может создаваться следующим образом:
var instance = await AsyncClass.CreateAsync();Главное преимущество этого паттерна заключается в том, что вызывающий код никак не сможет получить неинициализированный экземпляр
AsyncClass
.К сожалению, в некоторых сценариях этот способ не работает — в частности, когда в коде используется провайдер внедрения зависимостей. Ни одна заметная библиотека внедрения зависимостей или инверсии управления не работает с async-кодом. Если вы окажетесь в одной из таких ситуаций, существует пара альтернатив, которые также стоит рассмотреть.
Если создаваемый экземпляр в действительности является общим ресурсом, можно использовать ленивую инициализацию. В противном случае можно воспользоваться паттерном асинхронной инициализации, который рассмотрим позже.
Пример того, как поступать не следует:
class AsyncClassНа первый взгляд решение может показаться разумным: вы получаете обычный конструктор, который запускает асинхронную операцию; при этом у него есть ряд недостатков, обусловленных использованием async void. Первая проблема заключается в том, что при завершении конструктора экземпляр всё ещё продолжает асинхронно инициализироваться, и не существует очевидного способа определить, когда завершится асинхронная инициализация. Вторая проблема связана с обработкой ошибок: любые исключения, выданные из InitAsync, не могут быть перехвачены секциями catch, окружающими вызов конструктора объекта.
{
public AsyncClass()
{
InitAsync();
}
// ПЛОХОЙ КОД!!
private async void InitAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
👍12
День 1345. #ЗаметкиНаПолях #AsyncTips
Асинхронное Создание Объектов: Паттерн Асинхронной Инициализации
Задача
Вам нужен тип, требующий выполнения некоторой асинхронной работы в конструкторе, но вы не можете воспользоваться паттерном асинхронной фабрики, так как экземпляр создаётся с применением рефлексии (например, IoC-контейнера, связывания данных, Activator.CreateInstance и т. д.).
Решение
В таком сценарии вам приходится возвращать неинициализированный экземпляр, хотя ситуацию можно частично сгладить применением распространённого паттерна асинхронной инициализации. Каждый тип, требующий асинхронной инициализации, должен определять специальное свойство (обычно в интерфейсе-маркере):
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
Асинхронное Создание Объектов: Паттерн Асинхронной Инициализации
Задача
Вам нужен тип, требующий выполнения некоторой асинхронной работы в конструкторе, но вы не можете воспользоваться паттерном асинхронной фабрики, так как экземпляр создаётся с применением рефлексии (например, IoC-контейнера, связывания данных, Activator.CreateInstance и т. д.).
Решение
В таком сценарии вам приходится возвращать неинициализированный экземпляр, хотя ситуацию можно частично сгладить применением распространённого паттерна асинхронной инициализации. Каждый тип, требующий асинхронной инициализации, должен определять специальное свойство (обычно в интерфейсе-маркере):
public interface IAsyncInitПри реализации этого паттерна следует начать инициализацию, задав значение свойства
{
Task Initialization { get; }
}
Initialization
в конструкторе. Доступ к результатам асинхронной инициализации (вместе с любыми исключениями) предоставляется через свойство Initialization
. Пример реализации:class MyType : IAsyncInitЭкземпляр этого типа может быть создан и инициализирован примерно так:
{
public MyType()
{
Initialization = InitAsync();
}
public Task Initialization
{ get; private set; }
private async Task InitAsync()
{
// асинхронно инициализируем экземпляр
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
var instance = DIContainer.Resolve<MyType>();По возможности рекомендуется применять асинхронные фабрики или ленивую инициализацию вместо этого решения. Эти решения предпочтительны, потому что в них исключается доступ к неинициализированному экземпляру. Если ваши экземпляры создаются библиотеками внедрения зависимостей/инверсии управления, связывания данных и т. д., и вы вынуждены открыть доступ к неинициализированному экземпляру, придётся использовать паттерн асинхронной инициализации.
var asyncInit = instance as IAsyncInit;
if (asyncInit != null)
await asyncInit.Initialization;
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
👍6
День 1356. #ЗаметкиНаПолях #AsyncTips
Асинхронные Свойства
Задача: имеется свойство, которое вам хотелось бы объявить, как асинхронное. Свойство не задействовано в связывании данных.
Решение
Эта проблема часто встречается при преобразовании существующего кода для использования async; в таких ситуациях создаётся свойство, get-метод которого вызывает асинхронный метод. Вообще, такого понятия, как «асинхронное свойство», не существует. Ключевое слово async не может использоваться со свойством, и это хорошо. Get-методы свойств должны возвращать текущие значения; они не должны запускать фоновые операции:
Стоит задуматься над тем, как состояние соотносится с асинхронным кодом. Это особенно актуально при преобразовании синхронной кодовой базы в асинхронную. Возьмем любое состояние, доступ к которому осуществляется через API (например, через свойства). Для каждой составляющей состояния спросите себя: что считать текущим состоянием объекта с незавершенной асинхронной операцией? Правильного ответа не существует, но важно продумать то, какие семантики вам нужны и как их документировать.
Для примера возьмем объект Stream.Position, представляющий текущее смещение указателя в потоке. С синхронным API при вызове Stream.Read или Stream.Write чтение/запись завершается, а Stream.Position обновляется новой позицией перед возвращением управления методом Read или Write. Для синхронного кода семантика ясна.
Теперь возьмем Stream.ReadAsync и Stream.WriteAsync: когда должно обновляться значение Stream.Position? При завершении операции чтения/записи или до того, как это фактически произойдет? Если оно обновляется перед завершением операции, то будет ли оно обновлено синхронно к моменту возвращения управления ReadAsync/WriteAsync или же вскоре после этого?
Это отличный пример того, как свойство, предоставляющее доступ к состоянию, обладает абсолютно ясной семантикой для синхронного кода, но не имеет очевидно правильной семантики для асинхронного кода. Конечно, ничего ужасного в этом нет — просто нужно продумать весь API при реализации поддержки async для ваших типов и документировать выбранную вами семантику.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
Асинхронные Свойства
Задача: имеется свойство, которое вам хотелось бы объявить, как асинхронное. Свойство не задействовано в связывании данных.
Решение
Эта проблема часто встречается при преобразовании существующего кода для использования async; в таких ситуациях создаётся свойство, get-метод которого вызывает асинхронный метод. Вообще, такого понятия, как «асинхронное свойство», не существует. Ключевое слово async не может использоваться со свойством, и это хорошо. Get-методы свойств должны возвращать текущие значения; они не должны запускать фоновые операции:
// Чего хотелось бы (не компилируется)Когда вам кажется, что вам нужно «асинхронное свойство», в действительности требуется нечто иное. Если ваше «асинхронное свойство» должно запускать новое (асинхронное) вычисление каждый раз, когда оно читается, то, по сути, это замаскированный метод:
public int Data
{
async get
{
await Task.Delay(TimeSpan.FromSeconds(1));
return 13;
}
}
public async Task<int> GetDataAsync()Вы можете получить Task<int> непосредственно из свойства:
{
await Task.Delay(TimeSpan.FromSeconds(1));
return 42;
}
public Task<int> DataИ все же так делать не рекомендуется. Это свойство должно стать методом. Так это ясно показывает, что каждый раз запускается новая асинхронная операция, поэтому API не вводит пользователя в заблуждение.
{
get { return GetDataAsync(); }
}
Стоит задуматься над тем, как состояние соотносится с асинхронным кодом. Это особенно актуально при преобразовании синхронной кодовой базы в асинхронную. Возьмем любое состояние, доступ к которому осуществляется через API (например, через свойства). Для каждой составляющей состояния спросите себя: что считать текущим состоянием объекта с незавершенной асинхронной операцией? Правильного ответа не существует, но важно продумать то, какие семантики вам нужны и как их документировать.
Для примера возьмем объект Stream.Position, представляющий текущее смещение указателя в потоке. С синхронным API при вызове Stream.Read или Stream.Write чтение/запись завершается, а Stream.Position обновляется новой позицией перед возвращением управления методом Read или Write. Для синхронного кода семантика ясна.
Теперь возьмем Stream.ReadAsync и Stream.WriteAsync: когда должно обновляться значение Stream.Position? При завершении операции чтения/записи или до того, как это фактически произойдет? Если оно обновляется перед завершением операции, то будет ли оно обновлено синхронно к моменту возвращения управления ReadAsync/WriteAsync или же вскоре после этого?
Это отличный пример того, как свойство, предоставляющее доступ к состоянию, обладает абсолютно ясной семантикой для синхронного кода, но не имеет очевидно правильной семантики для асинхронного кода. Конечно, ничего ужасного в этом нет — просто нужно продумать весь API при реализации поддержки async для ваших типов и документировать выбранную вами семантику.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
👍12
День 1362. #ЗаметкиНаПолях #AsyncTips
Асинхронное освобождение
Задача: имеется тип с асинхронными операциями, который должен обеспечить освобождение своих ресурсов.
Решение
Есть два распространённых варианта действий.
1. Запрос на отмену всех текущих операций.
Такие типы, как файловые потоки и сокеты, отменяют все существующие операции чтения и записи при закрытии. Определив собственный CancellationTokenSource и передавая этот маркер внутренним операциям, можно сделать нечто похожее. В этом случае Dispose отменит операции, не ожидая их завершения:
При вызове Dispose будут отменены все существующие операции в вызывающем коде:
2. Асинхронное освобождение впервые появилось в C# 8.0. Появились интерфейс IAsyncDisposable и команда await using. Таким образом, типы, которые собирались выполнить асинхронную работу при освобождении, теперь получили такую возможность:
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
Асинхронное освобождение
Задача: имеется тип с асинхронными операциями, который должен обеспечить освобождение своих ресурсов.
Решение
Есть два распространённых варианта действий.
1. Запрос на отмену всех текущих операций.
Такие типы, как файловые потоки и сокеты, отменяют все существующие операции чтения и записи при закрытии. Определив собственный CancellationTokenSource и передавая этот маркер внутренним операциям, можно сделать нечто похожее. В этом случае Dispose отменит операции, не ожидая их завершения:
class MyClass : IDisposableВыше приведён упрощённый код. В реальном паттерне Dispose не всё так просто. А также стоит предоставить пользователю возможность передать собственный маркер CancellationToken (используя приём, описанный в этом посте).
{
private readonly CancellationTokenSource _сts =
new CancellationTokenSource();
public async Task<int> CalcAsync()
{
await Task.Delay(
TimeSpan.FromSeconds(2),
_сts.Token);
return 42;
}
public void Dispose()
{
_сts.Cancel();
}
}
При вызове Dispose будут отменены все существующие операции в вызывающем коде:
async Task UseMyClassAsync()Для некоторых типов (например, HttpClient) такая реализация работает вполне нормально. Однако иногда необходимо убедиться, что будут завершены все операции.
{
Task<int> task;
using (var resource = new MyClass())
{
task = resource.CalcAsync(default);
}
// Выдает OperationCanceledException.
var result = await task;
}
2. Асинхронное освобождение впервые появилось в C# 8.0. Появились интерфейс IAsyncDisposable и команда await using. Таким образом, типы, которые собирались выполнить асинхронную работу при освобождении, теперь получили такую возможность:
class MyClass : IAsyncDisposableИспользование:
{
public async ValueTask DisposeAsync()
{
await Task.Delay(TimeSpan.FromSeconds(2));
}
}
await using (var myClass = new MyClass())Также можно использовать ConfigureAwait(false):
{
…
} // <--
// Здесь вызывается DisposeAsync (с ожиданием)
var myClass = new MyClass();Асинхронное освобождение определенно проще, а первый подход должен использоваться только в том случае, если это действительно необходимо. Также при желании можно использовать оба подхода одновременно. Это наделит ваш тип семантикой «безопасного завершения работы», если в клиентском коде используется await using, и семантикой «жёсткой отмены», если клиентский код использует Dispose.
await using (myClass.ConfigureAwait(false))
{
…
} // <--
// Здесь вызывается DisposeAsync (с ожиданием)
// с ConfigureAwait(false).
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
👍11
День 1370. #ЗаметкиНаПолях #AsyncTips
Блокировки и команда lock
Задача
Имеются общие данные. Требуется обеспечить безопасное чтение и запись этих данных из нескольких потоков.
Решение
Лучшее решение в такой ситуации — использование команды блокировки lock. Если поток входит в блок lock, то все остальные потоки не смогут войти в этот блок, пока блокировка не будет снята:
При использовании блокировок следует руководствоваться четырьмя важными рекомендациями:
1. Ограничьте видимость блокировки.
Объект, используемый в команде lock, должен быть приватным полем, которое никогда не должно быть доступным извне класса. Обычно есть не более одного поля блокировки на тип; если у вас их несколько, рассмотрите возможность рефакторинга этого типа на несколько типов. Блокировка может устанавливаться по любому ссылочному типу, но рекомендуется создавать отдельное поле специально для команды lock, как в примере выше. Если вы устанавливаете блокировку по другому экземпляру, убедитесь в том, что он является приватным для вашего класса; он не должен передаваться в конструкторе или возвращаться из get-метода свойства. Никогда не используйте lock(this) или lock с любым экземпляром Type или string; это может привести к взаимоблокировкам, доступным из другого кода.
2. Оставьте комментарий, что именно защищает блокировка.
Об этом шаге легко забыть во время первоначального написания кода, но когда кодовая база вырастет, это может оказаться важным.
3. Сократите до минимума объем кода, защищённого блокировкой.
Один из аспектов, на которые следует обращать внимание, — блокирующие вызовы при удержании блокировок. В идеале их быть вообще не должно.
4. Никогда не выполняйте произвольный код при удержании блокировки.
Это включает выдачу событий, вызов виртуальных методов или вызов делегатов. Если вам потребуется выполнить произвольный код, сделайте это после снятия блокировки.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 12.
Блокировки и команда lock
Задача
Имеются общие данные. Требуется обеспечить безопасное чтение и запись этих данных из нескольких потоков.
Решение
Лучшее решение в такой ситуации — использование команды блокировки lock. Если поток входит в блок lock, то все остальные потоки не смогут войти в этот блок, пока блокировка не будет снята:
class MyClassВ .NET существует несколько механизмов блокировки: Monitor, Spin, Lock и ReaderWriterLockSlim. В большинстве приложений эти типы блокировок практически никогда не должны использоваться напрямую. В частности, разработчики часто используют ReaderWriterLockSlim, даже когда такая сложность не является необходимой. Базовая команда lock нормально справляется с 99% случаев.
{
// Блокировка защищает поле _value.
private readonly object _mutex = new object();
private int _value;
public void Increment()
{
lock (_mutex)
{
_value = _value + 1;
}
}
}
При использовании блокировок следует руководствоваться четырьмя важными рекомендациями:
1. Ограничьте видимость блокировки.
Объект, используемый в команде lock, должен быть приватным полем, которое никогда не должно быть доступным извне класса. Обычно есть не более одного поля блокировки на тип; если у вас их несколько, рассмотрите возможность рефакторинга этого типа на несколько типов. Блокировка может устанавливаться по любому ссылочному типу, но рекомендуется создавать отдельное поле специально для команды lock, как в примере выше. Если вы устанавливаете блокировку по другому экземпляру, убедитесь в том, что он является приватным для вашего класса; он не должен передаваться в конструкторе или возвращаться из get-метода свойства. Никогда не используйте lock(this) или lock с любым экземпляром Type или string; это может привести к взаимоблокировкам, доступным из другого кода.
2. Оставьте комментарий, что именно защищает блокировка.
Об этом шаге легко забыть во время первоначального написания кода, но когда кодовая база вырастет, это может оказаться важным.
3. Сократите до минимума объем кода, защищённого блокировкой.
Один из аспектов, на которые следует обращать внимание, — блокирующие вызовы при удержании блокировок. В идеале их быть вообще не должно.
4. Никогда не выполняйте произвольный код при удержании блокировки.
Это включает выдачу событий, вызов виртуальных методов или вызов делегатов. Если вам потребуется выполнить произвольный код, сделайте это после снятия блокировки.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 12.
👍17
День 1390. #ЗаметкиНаПолях #AsyncTips
Блокировки с async
Задача
Имеются общие данные. Требуется обеспечить безопасное чтение и запись этих данных из разных блоков кода, внутри которых может использоваться await.
Решение
Тип SemaphoreSlim был обновлён в .NET 4.5 для обеспечения совместимости с async. Пример использования:
В этой ситуации действуют рекомендации из предыдущего совета:
Экземпляры блокировок должны быть приватными; т.е. не должны быть доступными за пределами класса. Обязательно чётко документируйте (и тщательно продумывайте), что именно защищает экземпляр блокировки. Сведите к минимуму объём кода, выполняемого при удержании блокировки. В частности, не вызывайте произвольный код, включая выдачу событий, вызов виртуальных методов и вызов делегатов.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 12.
Блокировки с async
Задача
Имеются общие данные. Требуется обеспечить безопасное чтение и запись этих данных из разных блоков кода, внутри которых может использоваться await.
Решение
Тип SemaphoreSlim был обновлён в .NET 4.5 для обеспечения совместимости с async. Пример использования:
class MyClassТестовый вариант использования класса:
{
private int _value;
// Блокировка защищает поле _value
private readonly SemaphoreSlim
semaphore = new SemaphoreSlim(1);
public async Task IncAsync(string name)
{
await semaphore.WaitAsync();
try
{
int oldValue = _value;
await Task.Delay(1000 * oldValue);
_value = oldValue + 1;
Console.WriteLine($"{name}: {_value}");
}
finally
{
semaphore.Release();
}
}
}
var cls = new MyClass();Этот код будет выводить одну за другой строки с постоянно увеличивающейся задержкой между ними:
for (int i = 1; i <= 5; i++)
{
int count = i;
Thread t = new(
async () =>
await cls.IncAsync("Поток " + count)
);
t.Start();
}
Console.ReadLine();
Поток 4: 1Заметьте, что SemaphoreSlim принимает в качестве параметра конструктора количество потоков, которые одновременно могут получить доступ к блокировке. В данном случае мы задали 1.
Поток 3: 2
Поток 2: 3
Поток 5: 4
Поток 1: 5
В этой ситуации действуют рекомендации из предыдущего совета:
Экземпляры блокировок должны быть приватными; т.е. не должны быть доступными за пределами класса. Обязательно чётко документируйте (и тщательно продумывайте), что именно защищает экземпляр блокировки. Сведите к минимуму объём кода, выполняемого при удержании блокировки. В частности, не вызывайте произвольный код, включая выдачу событий, вызов виртуальных методов и вызов делегатов.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 12.
👍23
День 1398. #ЗаметкиНаПолях #AsyncTips
Блокирующие и Асинхронные Сигналы
1. Блокирующие Сигналы
Задача: требуется отправить уведомление от одного потока другому.
Решение
Самый распространённый и универсальный межпотоковый сигнал — событие с ручным сбросом ManualResetEventSlim. Оно может находиться в одном из двух состояний: установленном или сброшенном. Любой поток может перевести событие в установленное состояние или сбросить его. Поток также может ожидать перехода события в установленное состояние.
Следующие два метода вызываются разными потоками; один поток, использует Wait() и ожидает сигнала от другого, который вызывает Init():
2. Асинхронные Сигналы
Задача: требуется отправить уведомление от одного потока другому, при этом получатель оповещения должен ожидать его асинхронно.
Решение
Если уведомление должно быть отправлено только один раз, можно использовать TaskCompletionSource<T>. Код-отправитель вызывает TrySetResult, а код-получатель ожидает его свойство Task:
Итого
Сигналы представляют собой механизм уведомлений общего назначения, однако использовать их следует только тогда, когда это действительно уместно. Если «сигнал» представляет собой сообщение, отправляющее некоторые данные между потоками, рассмотрите возможность использования очереди «производитель/потребитель». А если сигналы используются только для координации доступа к общим данным, лучше использовать lock или асинхронную блокировку.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 12.
Блокирующие и Асинхронные Сигналы
1. Блокирующие Сигналы
Задача: требуется отправить уведомление от одного потока другому.
Решение
Самый распространённый и универсальный межпотоковый сигнал — событие с ручным сбросом ManualResetEventSlim. Оно может находиться в одном из двух состояний: установленном или сброшенном. Любой поток может перевести событие в установленное состояние или сбросить его. Поток также может ожидать перехода события в установленное состояние.
Следующие два метода вызываются разными потоками; один поток, использует Wait() и ожидает сигнала от другого, который вызывает Init():
class MyClassManualResetEventSlim является синхронным сигналом, поэтому WaitForInitialization блокирует вызывающий поток до отправки сигнала.
{
private readonly ManualResetEventSlim
_mres = new();
private int _val;
public int Wait()
{
_mres.Wait();
return _val;
}
public void Init()
{
_val = 42;
_mres.Set();
}
}
2. Асинхронные Сигналы
Задача: требуется отправить уведомление от одного потока другому, при этом получатель оповещения должен ожидать его асинхронно.
Решение
Если уведомление должно быть отправлено только один раз, можно использовать TaskCompletionSource<T>. Код-отправитель вызывает TrySetResult, а код-получатель ожидает его свойство Task:
class MyClassAsyncTaskCompletionSource<T> может использоваться для асинхронного ожидания любой ситуации — в данном случае уведомления от другой части кода. Этот способ хорошо работает, если сигнал отправляется только один раз, но не работает, если сигнал нужно не только включать, но и отключать.
{
private readonly TaskCompletionSource<object?>
_set = new();
private int _val1;
private int _val2;
public async Task<int> WaitAsync()
{
await _set.Task;
return _val1 + _val2;
}
public void Init()
{
_val1 = 42;
_val2 = 69;
_set.TrySetResult(null);
}
}
Итого
Сигналы представляют собой механизм уведомлений общего назначения, однако использовать их следует только тогда, когда это действительно уместно. Если «сигнал» представляет собой сообщение, отправляющее некоторые данные между потоками, рассмотрите возможность использования очереди «производитель/потребитель». А если сигналы используются только для координации доступа к общим данным, лучше использовать lock или асинхронную блокировку.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 12.
👍13
День 1406. #ЗаметкиНаПолях #AsyncTips
Планирование работы в пуле потоков
Любой блок кода должен выполняться в каком-то потоке. Планировщик (scheduler) решает, где должен выполняться тот или иной код. Обычно планировщик по умолчанию работает как надо. Например, оператор await в асинхронном коде автоматически возобновит выполнение метода в том же контексте, если только вы не переопределите это поведение.
Задача: имеется фрагмент кода, который должен выполняться в потоке из пула потоков.
Решение
В большинстве случаев следует использовать Task.Run. Следующий пример блокирует поток из пула на 2 секунды:
Task.Run идеально подходит для UI-приложений с продолжительной работой, которая не должна выполняться в UI-потоке. Тем не менее не используйте Task.Run в ASP.NET, если только вы не уверены в том, что делаете. В ASP.NET код обработки запросов уже выполняется в потоке из пула, так что перенесение его в другой поток из пула обычно нерационально.
Task.Run является фактической заменой для BackgroundWorker, Delegate.BeginInvoke и ThreadPool.QueueUserWorkItem. Ни один из этих старых API не следует использовать в новом коде; код с Task.Run намного проще писать и сопровождать.
Task.Run справляется с большинством задач, для которых используется Thread, так что в большинстве случаев Thread может заменяться на Task.Run (за редким исключением).
Параллельный код и код потоков данных выполняется в пуле потоков по умолчанию, поэтому обычно Task.Run не нужно использовать с кодом, выполняемым через Parallel, библиотекой TPL Dataflow или Parallel LINQ.
Если вы применяете динамический параллелизм, используйте Task.Factory.StartNew вместо Task.Run. Это необходимо из-за того, что у объекта Task, возвращаемого Task.Run, параметры по умолчанию настроены для асинхронного использования (т.е. для потребления в асинхронном или реактивном коде). Кроме того, он не поддерживает такие расширенные возможности, как задачи «родитель/потомок», типичные для динамического параллельного кода.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
Планирование работы в пуле потоков
Любой блок кода должен выполняться в каком-то потоке. Планировщик (scheduler) решает, где должен выполняться тот или иной код. Обычно планировщик по умолчанию работает как надо. Например, оператор await в асинхронном коде автоматически возобновит выполнение метода в том же контексте, если только вы не переопределите это поведение.
Задача: имеется фрагмент кода, который должен выполняться в потоке из пула потоков.
Решение
В большинстве случаев следует использовать Task.Run. Следующий пример блокирует поток из пула на 2 секунды:
Task task = Task.Run(() =>Task.Run также поддерживает возвращаемые значения и асинхронные лямбда-выражения. Задача, возвращаемая Task.Run в следующем коде, завершится через 2 секунды с результатом 13:
{
Thread.Sleep(2000);
});
Task<int> task = Task.Run(async () =>Task.Run возвращает объект Task (или Task<T>), который может естественным образом потребляться асинхронным или реактивным кодом.
{
await Task.Delay(2000);
return 13;
});
Task.Run идеально подходит для UI-приложений с продолжительной работой, которая не должна выполняться в UI-потоке. Тем не менее не используйте Task.Run в ASP.NET, если только вы не уверены в том, что делаете. В ASP.NET код обработки запросов уже выполняется в потоке из пула, так что перенесение его в другой поток из пула обычно нерационально.
Task.Run является фактической заменой для BackgroundWorker, Delegate.BeginInvoke и ThreadPool.QueueUserWorkItem. Ни один из этих старых API не следует использовать в новом коде; код с Task.Run намного проще писать и сопровождать.
Task.Run справляется с большинством задач, для которых используется Thread, так что в большинстве случаев Thread может заменяться на Task.Run (за редким исключением).
Параллельный код и код потоков данных выполняется в пуле потоков по умолчанию, поэтому обычно Task.Run не нужно использовать с кодом, выполняемым через Parallel, библиотекой TPL Dataflow или Parallel LINQ.
Если вы применяете динамический параллелизм, используйте Task.Factory.StartNew вместо Task.Run. Это необходимо из-за того, что у объекта Task, возвращаемого Task.Run, параметры по умолчанию настроены для асинхронного использования (т.е. для потребления в асинхронном или реактивном коде). Кроме того, он не поддерживает такие расширенные возможности, как задачи «родитель/потомок», типичные для динамического параллельного кода.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
👍19
День 1412. #ЗаметкиНаПолях #AsyncTips
Выполнение Кода с Помощью Планировщика. Начало: Создание Планировщика
Задача
Есть несколько частей кода, которые требуется выполнить определённым способом. Например, все они должны выполняться в UI-потоке или же в любой момент времени должно выполняться только определённое количество частей.
Решение
Здесь рассмотрим тип TaskScheduler, хотя, в .NET есть и другие способы решения этой задачи.
Простейшая разновидность — TaskScheduler.Default — ставит работу в очередь пула потоков. Его редко придётся использовать явно, потому что он используется по умолчанию во многих сценариях планирования: в Task.Run, в параллельном коде и в коде потоков данных.
Вы можете сохранить конкретный контекст и позднее спланировать работу в этом контексте:
ConcurrentExclusiveSchedulerPair — тип, представляющий в действительности два планировщика, связанных друг с другом:
- ConcurrentScheduler позволяет нескольким задачам выполняться одновременно, при условии, что ни одна задача не выполняется в ExclusiveScheduler;
- ExclusiveScheduler выполняет только по одной задаче за раз и только если в настоящее время никакие задачи не выполняются в ConcurrentScheduler.
Также тип может применяться в качестве регулирующего планировщика, который будет ограничивать собственный уровень параллелизма. При этом ExclusiveScheduler обычно не используется:
Заметьте, что конструктору ConcurrentExclusiveSchedulerPair передаётся объект TaskScheduler.Default. Это объясняется тем, что ConcurrentExclusiveSchedulerPair применяет свою логику конкурентности/эксклюзивности выполнения к существующему TaskScheduler.
Никогда не используйте платформенно-зависимые типы для выполнения кода в UI-потоке. WPF, Silverlight, iOS и Android предоставляют тип Dispatcher, Universal Windows использует тип CoreDispatcher, а в Windows Forms существует интерфейс ISynchronizeInvoke (т. е. Control.Invoke). Не используйте эти типы в новом коде; просто считайте, что их вообще нет. Эти типы только без всякой необходимости привязывают код к конкретной платформе. SynchronizationContext — абстракция общего назначения на базе этих типов.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
Выполнение Кода с Помощью Планировщика. Начало: Создание Планировщика
Задача
Есть несколько частей кода, которые требуется выполнить определённым способом. Например, все они должны выполняться в UI-потоке или же в любой момент времени должно выполняться только определённое количество частей.
Решение
Здесь рассмотрим тип TaskScheduler, хотя, в .NET есть и другие способы решения этой задачи.
Простейшая разновидность — TaskScheduler.Default — ставит работу в очередь пула потоков. Его редко придётся использовать явно, потому что он используется по умолчанию во многих сценариях планирования: в Task.Run, в параллельном коде и в коде потоков данных.
Вы можете сохранить конкретный контекст и позднее спланировать работу в этом контексте:
var scheduler =Этот код создаёт объект TaskScheduler, чтобы сохранить текущий контекст и спланировать выполнение кода в нём. Тип SynchronizationContext представляет контекст планирования общего назначения. В .NET предусмотрено несколько разных контекстов: многие UI-фреймворки предоставляют контекст SynchronizationContext, представляющий UI-поток, а в ASP.NET до Core предоставлялся контекст SynchronizationContext, представляющий контекст запроса HTTP. Также возможно напрямую использовать SynchronizationContext для выполнения кода в этом контексте; но это не рекомендуется. Там, где это возможно, используйте await для возобновления в неявно сохранённом контексте либо TaskScheduler.
TaskScheduler
.FromCurrentSynchronizationContext();
ConcurrentExclusiveSchedulerPair — тип, представляющий в действительности два планировщика, связанных друг с другом:
- ConcurrentScheduler позволяет нескольким задачам выполняться одновременно, при условии, что ни одна задача не выполняется в ExclusiveScheduler;
- ExclusiveScheduler выполняет только по одной задаче за раз и только если в настоящее время никакие задачи не выполняются в ConcurrentScheduler.
var schPair = new ConcurrentExclusiveSchedulerPair();Одно из частых применений этого типа — просто использование ExclusiveScheduler, гарантирующее, что в любой момент времени будет выполняться только одна задача. Код, выполняемый в ExclusiveScheduler, будет выполняться в пуле потоков эксклюзивно - без всего остального кода, использующего тот же экземпляр ExclusiveScheduler.
var concurrent = schPair.ConcurrentScheduler;
var exclusive = schPair.ExclusiveScheduler;
Также тип может применяться в качестве регулирующего планировщика, который будет ограничивать собственный уровень параллелизма. При этом ExclusiveScheduler обычно не используется:
var schPair = new ConcurrentExclusiveSchedulerPair(Заметьте, что такая регулировка влияет на код только во время его выполнения. В частности, асинхронный код не считается выполняемым во время ожидания операции. ConcurrentScheduler регулирует выполняющийся код; тогда как другие виды регулировки (такие, как SemaphoreSlim) осуществляют регулировку на более высоком уровне (т.е. всего async-метода.)
TaskScheduler.Default,
maxConcurrencyLevel: 8);
var scheduler = schPair.ConcurrentScheduler;
Заметьте, что конструктору ConcurrentExclusiveSchedulerPair передаётся объект TaskScheduler.Default. Это объясняется тем, что ConcurrentExclusiveSchedulerPair применяет свою логику конкурентности/эксклюзивности выполнения к существующему TaskScheduler.
Никогда не используйте платформенно-зависимые типы для выполнения кода в UI-потоке. WPF, Silverlight, iOS и Android предоставляют тип Dispatcher, Universal Windows использует тип CoreDispatcher, а в Windows Forms существует интерфейс ISynchronizeInvoke (т. е. Control.Invoke). Не используйте эти типы в новом коде; просто считайте, что их вообще нет. Эти типы только без всякой необходимости привязывают код к конкретной платформе. SynchronizationContext — абстракция общего назначения на базе этих типов.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
👍11
День 1413. #ЗаметкиНаПолях #AsyncTips
Выполнение Кода с Помощью Планировщика. Окончание: Использование Планировщика
Задача: требуется управлять выполнением отдельных фрагментов в параллельном коде.
Решение
После того как вы создадите экземпляр TaskScheduler (см. Создание Планировщика), можете включить его в набор параметров, передаваемых методу Parallel. Следующий код получает набор коллекций матриц, запускает несколько параллельных циклов и ограничивает общий параллелизм всех циклов одновременно независимо от количества матриц в каждом наборе:
Передать TaskScheduler коду Parallel LINQ (PLINQ) невозможно.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
Выполнение Кода с Помощью Планировщика. Окончание: Использование Планировщика
Задача: требуется управлять выполнением отдельных фрагментов в параллельном коде.
Решение
После того как вы создадите экземпляр TaskScheduler (см. Создание Планировщика), можете включить его в набор параметров, передаваемых методу Parallel. Следующий код получает набор коллекций матриц, запускает несколько параллельных циклов и ограничивает общий параллелизм всех циклов одновременно независимо от количества матриц в каждом наборе:
void RotateMatrices(Parallel.Invoke также получает экземпляр ParallelOptions, поэтому вы можете передать TaskScheduler при вызове Parallel.Invoke так же, как и для Parallel.ForEach. При выполнении динамического параллельного кода можно передать TaskScheduler непосредственно TaskFactory.StartNew или Task.ContinueWith.
IEnumerable<IEnumerable<Matrix>> collections,
float degrees)
{
var schPair = new ConcurrentExclusiveSchedulerPair(
TaskScheduler.Default,
maxConcurrencyLevel: 8);
var scheduler = schPair.ConcurrentScheduler;
var opts = new ParallelOptions {
TaskScheduler = scheduler };
Parallel.ForEach(
collections,
opts,
matrices => Parallel.ForEach(
matrices,
opts,
m => m.Rotate(degrees)));
}
Передать TaskScheduler коду Parallel LINQ (PLINQ) невозможно.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
👍3
День 2010. #ЗаметкиНаПолях #AsyncTips
Конечный Автомат в C# для async/await. Начало
Часто говорят, что ключевые слова async/await приводят к созданию конечного автомата. Но что это значит? Рассмотрим на простом примере:
Здесь несколько вызовов await: для получения ответа, и для чтения содержимого.
Деление метода по границе await
Каждый раз при вызове await, мы знаем, что нам не нужно ничего делать, кроме как ждать результата. Логично при этом просто выйти из метода и вернуться, как только будет получен результат. Компилятор разделит метод по границе await и создаст конечный автомат. Вот упрощённый код:
Это очень упрощенная версия того, что делает компилятор, и она не учитывает важные части, например, как содержимое извлекается из HttpClient. Важно то, что мы синхронно вызываем часть метода до await, а затем используем механизм обратных вызовов (именно поэтому используется ref this) для продолжений метода. Таким образом, как только HTTP-вызов завершается, мы возвращаемся в метод и продолжаем с того места, где остановились (state = 1), когда завершается чтение – продолжаем со state=2.
Окончание следует…
Источник: https://steven-giesel.com/blogPost/720a48fd-0abe-4c32-83ac-26926d501895/the-state-machine-in-c-with-asyncawait
Конечный Автомат в C# для async/await. Начало
Часто говорят, что ключевые слова async/await приводят к созданию конечного автомата. Но что это значит? Рассмотрим на простом примере:
async Task<Dto> GetAsync()
{
using var hc = new HttpClient();
hc.BaseAddress = new Uri("…");
var resp = await hc.GetAsync("");
resp.EnsureSuccessStatusCode();
var content =
await resp.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<Dto>(content);
}
public class Dto;
Здесь несколько вызовов await: для получения ответа, и для чтения содержимого.
Деление метода по границе await
Каждый раз при вызове await, мы знаем, что нам не нужно ничего делать, кроме как ждать результата. Логично при этом просто выйти из метода и вернуться, как только будет получен результат. Компилятор разделит метод по границе await и создаст конечный автомат. Вот упрощённый код:
public class GetAllAsync_StateMachine
{
public ContinuationMachine _builder =
ContinuationMachineBuilder.Create();
private int _state = 0;
private HttpClient _hc;
private HttpResponseMessage _resp;
private string _content;
private void MoveNext()
{
switch (_state)
{
case 0:
_hc = new HttpClient();
_hc.BaseAddress = new Uri("…");
_hc.GetAsync("");
_state = 1;
_builder.Continue(ref this);
break;
case 1:
_resp.EnsureSuccessStatusCode();
_resp.Content.ReadAsStringAsync();
_state = 2;
_builder.Continue(ref this);
break;
case 2:
return JsonSerializer.Deserialize<Dto>(_content);
}
}
}
Это очень упрощенная версия того, что делает компилятор, и она не учитывает важные части, например, как содержимое извлекается из HttpClient. Важно то, что мы синхронно вызываем часть метода до await, а затем используем механизм обратных вызовов (именно поэтому используется ref this) для продолжений метода. Таким образом, как только HTTP-вызов завершается, мы возвращаемся в метод и продолжаем с того места, где остановились (state = 1), когда завершается чтение – продолжаем со state=2.
Окончание следует…
Источник: https://steven-giesel.com/blogPost/720a48fd-0abe-4c32-83ac-26926d501895/the-state-machine-in-c-with-asyncawait
👍21
День 2011. #ЗаметкиНаПолях #AsyncTips
Конечный Автомат в C# для async/await. Окончание
Начало
Планировщик заданий
Продолжения (обратные вызовы) размещаются в планировщике заданий (TaskScheduler). Он берёт конечный автомат и планирует его выполнение после завершения ожидаемой задачи. Таким образом, TaskScheduler отвечает за продолжения. Здесь есть ещё несколько интересных моментов, таких как контекст синхронизации (SynchronizationContext) и пул потоков (ThreadPool), но это детали.
Итак: при использовании async/await компилятор разделит метод по границам await и создаст конечный автомат. Этот конечный автомат будет запланирован в TaskScheduler для продолжения после завершения ожидаемой задачи.
Где хранится конечный автомат?
В общем случае - в Task или Task<T>. Там хранится текущее состояние (включая продолжения), а также результат ожидаемой задачи. Но, кроме того, объект Task также хранит исключения. Исключения в с async/await-коде немного отличаются от исключений в синхронном коде.
Рассмотрим следующий код:
Компилятор преобразует этот код в следующий:
Важно отметить, что внутри блока catch нет throw. Т.е., если возникнет исключение в асинхронной части метода, оно не будет выброшено, а будет сохранено в объекте Task.
Возможно, теперь вы понимаете, почему async void — плохая идея: исключения будут потеряны. Они будут возникать, но вы не сможете их перехватить или обработать каким-либо образом. То же самое относится к async Task, если вы не ожидаете её. Поэтому:
Исключение перехватывается и сохраняется в объекте Task, не выбрасываясь во внешний код. Но когда вы ожидаете объект Task, вызов await будет преобразован во что-то вроде GetAwaiter().GetResult(), и именно здесь исключение выбрасывается из объекта Task наружу.
Источник: https://steven-giesel.com/blogPost/720a48fd-0abe-4c32-83ac-26926d501895/the-state-machine-in-c-with-asyncawait
Конечный Автомат в C# для async/await. Окончание
Начало
Планировщик заданий
Продолжения (обратные вызовы) размещаются в планировщике заданий (TaskScheduler). Он берёт конечный автомат и планирует его выполнение после завершения ожидаемой задачи. Таким образом, TaskScheduler отвечает за продолжения. Здесь есть ещё несколько интересных моментов, таких как контекст синхронизации (SynchronizationContext) и пул потоков (ThreadPool), но это детали.
Итак: при использовании async/await компилятор разделит метод по границам await и создаст конечный автомат. Этот конечный автомат будет запланирован в TaskScheduler для продолжения после завершения ожидаемой задачи.
Где хранится конечный автомат?
В общем случае - в Task или Task<T>. Там хранится текущее состояние (включая продолжения), а также результат ожидаемой задачи. Но, кроме того, объект Task также хранит исключения. Исключения в с async/await-коде немного отличаются от исключений в синхронном коде.
Рассмотрим следующий код:
static async Task ThrowExceptionAsync()
{
await Task.Yield();
throw new Exception("Ex");
}
Компилятор преобразует этот код в следующий:
try
{
YieldAwaitable.YieldAwaiter awaiter;
// Другой код
awaiter.GetResult();
throw new Exception("Ex");
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
}
Важно отметить, что внутри блока catch нет throw. Т.е., если возникнет исключение в асинхронной части метода, оно не будет выброшено, а будет сохранено в объекте Task.
Возможно, теперь вы понимаете, почему async void — плохая идея: исключения будут потеряны. Они будут возникать, но вы не сможете их перехватить или обработать каким-либо образом. То же самое относится к async Task, если вы не ожидаете её. Поэтому:
static async Task ThrowExAsync()
{
throw new Exception("Ex");
await SomethingAsync();
}
// не выбросит исключения
_ = ThrowExAsync();
// не выбросит исключения
ThrowExAsync();
// выбросит исключение
await ThrowExAsync();
Исключение перехватывается и сохраняется в объекте Task, не выбрасываясь во внешний код. Но когда вы ожидаете объект Task, вызов await будет преобразован во что-то вроде GetAwaiter().GetResult(), и именно здесь исключение выбрасывается из объекта Task наружу.
Источник: https://steven-giesel.com/blogPost/720a48fd-0abe-4c32-83ac-26926d501895/the-state-machine-in-c-with-asyncawait
👍16👎1