День 1231. #ЗаметкиНаПолях #AsyncTips
Блокирующие Стеки и Множества
Задача
Требуется коммуникационный канал для передачи сообщений или данных из одного потока в другой, но вы не хотите, чтобы этот канал использовал семантику FIFO.
Решение
Тип .NET BlockingCollection<T> по умолчанию работает как блокирующая очередь, но он также может работать как любая другая коллекция «производитель/потребитель». По сути, это обёртка для потокобезопасной коллекции, реализующей
Таким образом, вы можете создать
Всё, чтобы было сказано о регулировке применительно к блокирующим очередям, также применимо к блокирующим стекам или множествам. Если ваши производители работают быстрее потребителей, и вы хотите ограничить использование памяти блокирующим стеком/очередью, используйте регулировку (о ней в будущих постах).
Здесь для кода-потребителя используется
Подробнее о потокобезопасных коллекциях см. 1, 2, 3
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
Блокирующие Стеки и Множества
Задача
Требуется коммуникационный канал для передачи сообщений или данных из одного потока в другой, но вы не хотите, чтобы этот канал использовал семантику FIFO.
Решение
Тип .NET BlockingCollection<T> по умолчанию работает как блокирующая очередь, но он также может работать как любая другая коллекция «производитель/потребитель». По сути, это обёртка для потокобезопасной коллекции, реализующей
IProducerConsumerCollection<T>.Таким образом, вы можете создать
BlockingCollection<T> с семантикой LIFO или семантикой неупорядоченного множества:var blockStack = new BlockingCollection<int>(Важно учитывать, что с упорядочением элементов связаны некоторые условия гонки. Если код-производитель отработает до любого кода-потребителя, порядок элементов будет таким же, как у стека:
new ConcurrentStack<int>());
var blockBag = new BlockingCollection<int>(
new ConcurrentBag<int>());
// Код-производительЕсли код-производитель и код-потребитель выполняются в разных потоках (как это обычно бывает), потребитель всегда получает следующим тот элемент, который был добавлен последним. Например, производитель добавляет
blockStack.Add(7);
blockStack.Add(13);
blockStack.CompleteAdding();
// Код-потребитель
// Выводит "13", затем "7".
foreach (int item in blockStack.GetConsumingEnumerable())
Console.WriteLine(item);
7, потребитель получает 7, затем производитель добавляет 13, потребитель получает 13. Потребитель не ожидает вызова CompleteAdding перед тем, как вернуть первый элемент.Всё, чтобы было сказано о регулировке применительно к блокирующим очередям, также применимо к блокирующим стекам или множествам. Если ваши производители работают быстрее потребителей, и вы хотите ограничить использование памяти блокирующим стеком/очередью, используйте регулировку (о ней в будущих постах).
Здесь для кода-потребителя используется
GetConsumingEnumerable. Это самый распространённый сценарий. Также существует метод Take, который позволяет потребителю получить только один элемент (вместо потребления всех элементов).Подробнее о потокобезопасных коллекциях см. 1, 2, 3
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
👍7
День 1246. #ЗаметкиНаПолях #AsyncTips
Асинхронные очереди
Задача
Требуется коммуникационный канал для передачи сообщений или данных из одной части кода в другую по принципу FIFO без блокирования потоков.
Например, один фрагмент кода может загружать данные, которые отправляются по каналу по мере загрузки; при этом UI-поток получает данные и выводит их.
Решение
Требуется очередь с асинхронным API. В базовом фреймворке .NET такого типа нет, но в NuGet есть пара возможных решений.
1. System.Threading.Channels
Библиотека для асинхронных коллекций «производитель/потребитель» с акцентом на быстродействие. Производители записывают элементы в канал вызовом
2. System.Threading.Tasks.Dataflow
См. также
- очереди «производитель/потребитель» с блокирующей семантикой вместо асинхронной.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
Асинхронные очереди
Задача
Требуется коммуникационный канал для передачи сообщений или данных из одной части кода в другую по принципу FIFO без блокирования потоков.
Например, один фрагмент кода может загружать данные, которые отправляются по каналу по мере загрузки; при этом UI-поток получает данные и выводит их.
Решение
Требуется очередь с асинхронным API. В базовом фреймворке .NET такого типа нет, но в NuGet есть пара возможных решений.
1. System.Threading.Channels
Библиотека для асинхронных коллекций «производитель/потребитель» с акцентом на быстродействие. Производители записывают элементы в канал вызовом
WriteAsync, а когда завершают производство элементов, один из них вызывает Complete для уведомления канала о том, что в дальнейшем элементов больше не будет:var queue = Channel.CreateUnbounded<int>();Код-потребитель использует асинхронные потоки.
// Код-производитель
var writer = queue.Writer;
await writer.WriteAsync(7);
await writer.WriteAsync(13);
writer.Complete();
// Код-потребитель
// Выводит "7", затем "13".
var reader = queue.Reader;
await foreach (int value in reader.ReadAllAsync())
Console.WriteLine(value);
2. System.Threading.Tasks.Dataflow
BufferBlock<T> из библиотеки TPL Dataflow имеет много общего с каналом:var queue = new BufferBlock<int>();Код-потребитель использует метод
// Код-производитель
await queue.SendAsync(7);
await queue.SendAsync(13);
queue.Complete();
// Код-потребитель.
// Выводит "7", затем "13".
while (await queue.OutputAvailableAsync())
Console.WriteLine(await queue.ReceiveAsync());
OutputAvailableAsync, который на самом деле полезен только с одним потребителем. Если потребителей несколько, может случиться, что OutputAvailableAsync вернет true для нескольких потребителей, хотя элемент только один. Если очередь завершена, то ReceiveAsync выдаст исключение InvalidOperationException. Таким образом, для нескольких потребителей код будет выглядеть так:while (true)Библиотека Channels больше подходит для асинхронных очередей «производитель/потребитель» там, где это возможно. Помимо регулировки для случаев, когда производители работают быстрее потребителей, поддерживаются несколько режимов выборки (об этом в следующих постах). Однако, если логика вашего приложения может быть выражена в виде «конвейера», через который проходят данные, TPL Dataflow может быть более логичным кандидатом.
{
int item;
try
{
item = await queue.ReceiveAsync();
}
catch (InvalidOperationException)
{
break;
}
Console.WriteLine(item);
}
См. также
- очереди «производитель/потребитель» с блокирующей семантикой вместо асинхронной.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
👍5
День 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