Удаление IAsyncEnumerator не отменяет базовый токен отмены IAsyncEnumerable

Похоже, что удаление сгенерированных IAsyncEnumerableIAsyncEnumerator не запускает токен отмены базового IAsyncEnumerable. В частности, этот модульный тест не выполняет единственное утверждение:

[Test]
public async Task test()
{
    await foreach (var value in TestData())
        break;
}

private static async IAsyncEnumerable<int> TestData([EnumeratorCancellation] CancellationToken ct = default)
{
    try
    {
        yield return 1;
        await Task.CompletedTask;
        yield return 2;
    }
    finally
    {
        Assert.That(ct.IsCancellationRequested, Is.True);
    }
}

Я крайне удивлен таким поведением. Существует ли какой-либо стандартный подход к отмене CancellationToken TestData в этом сценарии? Я мог бы реализовать собственный оператор, но это кажется крайне неуклюжим, поэтому я надеюсь, что есть какой-нибудь лучший способ сделать это.

[Test]
public async Task this_test_passes()
{
    await foreach (var value in CancelWhenUnsubscribed(TestData()))
        break;
}

public static async IAsyncEnumerable<T> CancelWhenUnsubscribed<T>(IAsyncEnumerable<T> source, [EnumeratorCancellation] CancellationToken ct = default)
{
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
    await using var en = source.GetAsyncEnumerator(cts.Token);
    using var _ = Disposable.Create(() => cts.Cancel());
    while (await en.MoveNextAsync())
        yield return en.Current;
}

Для контекста, почему я пытаюсь это сделать: я использую System.Interactive.Async для объединения нескольких последовательностей IAsyncEnumerable.

Его реализация работает таким образом, что при удалении объединенной последовательности он сначала ожидает завершения всех выполняемых задач MoveNext и только затем удаляет и завершает объединенную последовательность.

В моем конкретном случае это означает, что мне нужно отменить базовый IAsyncEnumerables, когда объединенный объект завершается, иначе я столкнусь с квази-тупиками. В качестве простого примера, этот тест зависает:

[Test]
public async Task this_test_gets_stuck()
{
    await foreach (var value in AsyncEnumerableEx.Merge([TestData(), TestData()]))
        break;
}

private static async IAsyncEnumerable<int> TestData([EnumeratorCancellation] CancellationToken ct = default)
{
    yield return 1;
    await Task.Delay(TimeSpan.FromHours(100), ct);
    yield return 2;
}

Больше не читать последовательность: это не отмена. Это разные понятия, и, по моему мнению, они работают правильно. Более того, поскольку токен передается извне через WithCancellation, итератор буквально не может инициировать отмену: для этого вам нужен CTS, а не только CT. У вас уже есть finally — возможно, вы сможете создать свой собственный «связанный» CTS, который дополнительно будет запускаться с finally?

Marc Gravell 26.06.2024 09:26

Токен отмены не передается TestData, так что же удивлять? CancellationToken ct = default означает, что ct имеет значение по умолчанию: Нет. Отмена инициируется CancellationTokenSource, а не самим CancellationToken. CancellationToken используется только для наблюдения за отменой.

Panagiotis Kanavos 26.06.2024 10:47

@MarcGravell Хорошая мысль. Поскольку TestData переписана компилятором, я полагаю, что в странном мире можно было бы переписать ее таким образом, чтобы cancelToken, на который ссылается функция, был бы заменен токеном, который связан с исходным аргументом, но также удален на выходе. Но, по общему признанию, если подумать о точной семантике, это, вероятно, было бы более удивительным / сделало бы невозможным различие между выходом и отменой (хотя я не уверен, что могу вспомнить много разумных реальных случаев использования, когда кто-то предпочел бы, чтобы токен не был отменен. на выходе)

Bogey 26.06.2024 15:09

@Bogey, в конечном счете, отмена существует, чтобы обеспечить отмену во время асинхронных операций; простое прекращение итерации происходит вне асинхронного потока и уже хорошо поддерживается через finally

Marc Gravell 26.06.2024 15:16
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я не понимаю, почему ты удивляешься. Итератор TestData не владеет CancellationToken, поэтому он не обязан его отменять, даже если бы мог. На самом деле он не может его отменить по причинам, обсуждаемым в этом вопросе:

Если вы вызываете TestData без передачи CancellationToken и без использования метода расширения WithCancellation , то ct.CanBeCanceled будет false. В этом случае было бы абсурдно ожидать автоматической отмены токена, который не может быть отменен.

[1/2] Скажем так, для меня это удивительно на интуитивном уровне. Если вы покажете, например. мой последний пример (слияние) для случайной выборки разработчиков, я не уверен, сколько из них заметят проблему/зависание вызова (я, по общему признанию, этого не заметил). Но вы правы в том смысле, что если подумать о том, как это точно преобразуется в скомпилированный сгенерированный код и работает с точки зрения задействованных IAsyncEnumerators, это становится несколько более понятным.

Bogey 26.06.2024 15:10

[2/2] Мое очень личное мнение таково, что такое поведение все еще немного неприятно, поскольку, по крайней мере, лично у меня нет абсолютно никаких случаев использования, в которых я бы не хотел отменять операцию при выходе (и полагаю, что это, вероятно, более желательное поведение). почти во всех случаях), так что это немного «ощущается» как нечто, для чего, возможно, должно существовать ясное, краткое и простое нестандартное решение - но это, конечно, то, что есть.

Bogey 26.06.2024 15:13

@Богги, мы можем только догадываться. Я лично предполагаю, что почти 100% случайно выбранных разработчиков, знакомых с CancellationToken, ожидают, что он не будет автоматически отменен. Честно говоря, это больше похоже на личное заблуждение. Это нормально. У каждого есть несколько таких, в том числе и у меня.

Theodor Zoulias 26.06.2024 15:39

Другие вопросы по теме