Отмена асинхронных задач в C#

Постановка задачи

Я разрабатываю пользовательский интерфейс многоплатформенного приложения (MAUI). Я использую инструментарий сообщества, чтобы привязать команду к событию TextChanged панели поиска. В двух словах, эта команда ищет элементы в List<string>, который начинается со свойства searchbar.Text. Соответствующий код ниже:

// Occurring methods and variables
[ObservableProperty]
List<string> selectedTopicNames;

CancellationTokenSource cts = new();

List<string> GetNames() => topics.TopicsData.Select(element => element.Name).ToList();

// The actual method
[RelayCommand]
async Task PerformSearchAsync(string keyWord)
{
    await cts.CancelAsync();
    cts = new();
    // Fire and forget a task to search for the keyword
    _ = Task.Run(() => 
    {
        try 
        {
            // Get all names
            List<string> names = GetNames();

            // Perform case-insensitive search
            List<string> result = names.Where(name => 
            {
                cts.Token.ThrowIfCancellationRequested();
                return name.StartsWith(keyWord, StringComparison.OrdinalIgnoreCase);
            }).ToList();

            if (!result.SequenceEqual(SelectedTopicNames))
            {
                SelectedTopicNames = result;
            }
        }
        catch(OperationCanceledException) { }
    });
}

Основная идея состоит в том, чтобы запустить и забыть задачу, которая выполняет операцию поиска и перезаписывает свойство SelectedTopicNames, привязанное к CollectionView. Я нашел это хорошим решением, потому что когда вы переназначаете SelectedTopicNames, оно запускает событие OnPropertyChanged, которое уведомляет пользовательский интерфейс о том, что пришло время обновить отображаемые данные. Это может занять некоторое время, поскольку в худшем случае необходимо добавить кнопку 90 в пользовательский интерфейс.

Поскольку задача должна записать SelectedTopicNames, я подумал, что было бы неплохо отменить текущую задачу, чтобы избежать возможной одновременной записи и ненужного запуска события OnPropertyChanged.

Меня беспокоит то, что, согласно документации CancelAsync(), он будет только ждать процесса передачи сигнала. завершение зарегистрированных обратных вызовов, поэтому он не будет ожидать фактической отмены задачи. В этом случае, если текущая задача не может попасть на линию ThrowIfCancellationRequested между сигнализацией и повторной инициализацией источника токена отмены, то она не будет отменена и будет запущена параллельная задача.

Неудачные решения

  1. В настоящее время, как вы можете видеть, я поместил ThrowIfCancellationRequested внутри запроса LINQ, потому что это единственное место, где я могу его эффективно использовать. Потому что GetNames() также является LINQ, и нет смысла делать его асинхронным, чтобы его можно было отменить, и SequenceEqual также не поддерживает отмену. Этот подход не гарантирует, что задача будет отменена.
  2. Я считаю, что использование обратных вызовов также является тупиком, поскольку, согласно документации, вы не можете отменить текущие задачи, а скорее используются для очистки неконтролируемых ресурсов.

Желаемое поведение

Я хочу добиться такого поведения, если команда вызывается до завершения текущей задачи, тогда текущая задача немедленно отменяется или, по крайней мере, не выполняет назначение SelectedTopicNames = result;. (Основная цель — не создавать событие OnPropertyChanged, поскольку это подразумевает обновление пользовательского интерфейса.)

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
0
94
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Ваш код, скорее всего, должен выглядеть так

await cts.CancelAsync();
cts = new();
var token = cts.Token;
try{
    var selectedTopicNames = await Task.Run(() => 
    {
        
            // Get all names
            List<string> names = GetNames();

            // Perform case-insensitive search
            List<string> result = names.Where(name => 
            {
                token.ThrowIfCancellationRequested();
                return name.StartsWith(keyWord, StringComparison.OrdinalIgnoreCase);
            }).ToList();

            if (!result.SequenceEqual(SelectedTopicNames))
            {
                return result;
            }
            return null;        
    });
    token.ThrowIfCancellationRequested();
    if (selectedTopicNames != null){
        SelectedTopicNames = selectedTopicNames;
    }
}
catch(OperationCanceledException) { }

Это устраняет несколько проблем. Прежде всего, свойство корректно обновляется в потоке пользовательского интерфейса. А поскольку токен отмены проверяется в потоке пользовательского интерфейса, установка свойства и отмена будут правильно упорядочены.

Поскольку GetNames() также является LINQ.

GetNames() возвращает List<string>, так что в этом нет ничего ленивого или LINQy, это просто простой список в памяти.

Обратите внимание, что я бы рекомендовал провести некоторые измерения производительности. Эмпирическое правило — не блокировать поток пользовательского интерфейса более чем на 50 мс, и быстрый тест показывает, что это должно равняться примерно 200 тыс. строк. Итак, сколько у вас строк и сколько времени занимает ваша операция? Действительно ли необходимы отмена и потоковая обработка?

В этом случае может ли токен выдать OperationalException, даже если задача (которая предоставляет результат selectedTopicNames) еще не завершена? Я не уверен, что резьба нужна. Раньше, когда я не использовал многопоточность, добавление кнопок в пользовательский интерфейс происходило очень медленно и блокировало пользовательский интерфейс. Но в этом случае структура пользовательского интерфейса была другой, чем сейчас, например, представление коллекции было частью комбинации представления прокрутки и макета вертикального стека, и я читал об этом плохие отзывы, потому что это может испортить поддержку виртуализации.

supersonic-developer 07.06.2024 09:29

@supersonic-developer Ничто после await Task.Run не будет выполняться до тех пор, пока ожидаемая задача не завершится, и если задача завершится с исключением, оно будет повторно выдано ожиданием. Я рекомендую измерить/оценить/профилировать код перед любой оптимизацией и попытаться найти алгоритмические улучшения или избегать выполнения ненужной работы, прежде чем бросаться в потоки, решающие проблему.

JonasH 07.06.2024 09:45

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