День 1222. #ЗаметкиНаПолях #AsyncTips
Блокирующие очереди
Задача
Необходимо создать коммуникационный канал для передачи сообщений или данных между потоками. Например, один поток может загружать данные, которые отправляются по каналу по мере загрузки; другие потоки на стороне получения получают эти данные и обрабатывают их.
Решение
Тип .NET
Блокирующая очередь должна совместно использоваться несколькими потоками, и обычно определяется как приватное поле, доступное только для чтения:
Потоки-производители могут добавлять элементы вызовами Add, а когда поток-производитель завершится (когда будут добавлены все элементы), он может завершить коллекцию вызовом
В следующем простом примере производитель добавляет два элемента, а потом помечает коллекцию как завершенную:
Во всех приведенных примерах
При использовании таких коммуникационных каналов необходимо подумать о том, что произойдет, если производители работают быстрее потребителей. Если элементы производятся быстрее, чем потребляются, возможно, придётся применить регулировку очереди.
Блокирующие очереди хорошо работают при наличии отдельного потока (например, из пула потоков), действующего как производитель или потребитель. Они не настолько хороши, если вы хотите обращаться к коммуникационному каналу асинхронно — например, если UI-поток должен действовать в режиме потребителя. Если вы вводите в своё приложение подобный коммуникационный канал, подумайте о переходе на библиотеку TPL Dataflow. Во многих случаях решение с использованием TPL Dataflow проще самостоятельного построения коммуникационных каналов и фоновых потоков.
Тип
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
Блокирующие очереди
Задача
Необходимо создать коммуникационный канал для передачи сообщений или данных между потоками. Например, один поток может загружать данные, которые отправляются по каналу по мере загрузки; другие потоки на стороне получения получают эти данные и обрабатывают их.
Решение
Тип .NET
BlockingCollection<T> проектировался для создания таких коммуникационных каналов. По умолчанию BlockingCollection<T> работает в режиме блокирующей очереди и предоставляет поведение «первым зашел, первым вышел».Блокирующая очередь должна совместно использоваться несколькими потоками, и обычно определяется как приватное поле, доступное только для чтения:
private readonly BlockingCollection<int> _bq =Обычно поток делает что-то одно: либо добавляет элементы в коллекцию, либо удаляет элементы. Потоки, добавляющие элементы, называются потоками-производителями, а потоки, удаляющие элементы, называются потоками-потребителями.
new BlockingCollection<int>();
Потоки-производители могут добавлять элементы вызовами Add, а когда поток-производитель завершится (когда будут добавлены все элементы), он может завершить коллекцию вызовом
CompleteAdding. Тем самым он уведомляет коллекцию о том, что элементы далее добавляться не будут, а коллекция может сообщить своим потребителям, что элементов больше не будет.В следующем простом примере производитель добавляет два элемента, а потом помечает коллекцию как завершенную:
_bq.Add(7);Потоки-потребители обычно выполняются в цикле, ожидая следующего элемента и выполняя его последующую обработку. Если выделить код производителя в отдельный поток (например, вызовом
_bq.Add(13);
_bq.CompleteAdding();
Task.Run), то эти элементы можно будет потреблять следующим образом:// Выводит "7", затем "13".Если потребителей должно быть несколько,
foreach (int item in _bq.GetConsumingEnumerable())
Console.WriteLine(item);
GetConsumingEnumerable может вызываться из нескольких потоков одновременно. Тем не менее каждый элемент передается только одному из этих потоков. При завершении коллекции завершается и перечисляемый объект.Во всех приведенных примерах
GetConsumingEnumerable используется для потоков-потребителей; это самая распространённая ситуация. Но существует и метод Take, который позволяет потребителю получить только один элемент (вместо потребления всех элементов в цикле).При использовании таких коммуникационных каналов необходимо подумать о том, что произойдет, если производители работают быстрее потребителей. Если элементы производятся быстрее, чем потребляются, возможно, придётся применить регулировку очереди.
Блокирующие очереди хорошо работают при наличии отдельного потока (например, из пула потоков), действующего как производитель или потребитель. Они не настолько хороши, если вы хотите обращаться к коммуникационному каналу асинхронно — например, если UI-поток должен действовать в режиме потребителя. Если вы вводите в своё приложение подобный коммуникационный канал, подумайте о переходе на библиотеку TPL Dataflow. Во многих случаях решение с использованием TPL Dataflow проще самостоятельного построения коммуникационных каналов и фоновых потоков.
Тип
BufferBlock<T> из TPL Dataflow может работать как блокирующая очередь, к тому же TPL Dataflow позволяет построить конвейер или сеть для обработки. Впрочем, во многих простых случаях обычные блокирующие очереди (например, BlockingCollection<T>) станут более подходящим вариантом при проектировании.Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 9.
👍2
День 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