Обеспечит ли задержка между отменой и удалением CancellationTokenSource какую-либо уверенность в том, что для IsCancellationRequested будет установлено значение true?

#c# #uwp #async-await #cancellationtokensource #cancellation-token

#c# #uwp #async-await #cancellationtokensource #отмена-токен

Вопрос:

C # Проект Windows UWP

Я реализовал CancellationTokenSource и CancellationToken в асинхронном методе, который вызывает другой метод. Этот метод содержит цикл while, который поддерживает значение переменной bool как true до тех пор, пока источник токена не будет отменен.

Метод async запускается событием нажатия левой кнопки мыши и отменяется событием отпускания левой кнопки мыши, возникающим при использовании элемента управления ColorPicker. Переменная bool позволяет отправлять значения цвета на устройство при значении true и предотвращает его при значении false.

Поддерживая значение как true, устройство непрерывно получает изменяющиеся значения цвета, пока указатель перемещается вокруг средства выбора цвета, пока кнопка мыши остается нажатой. Как только кнопка мыши отпущена, результирующее значение false (которое устанавливается подпрограммой, которая отправляет значения цвета на устройство) предотвращает отправку дальнейших цветовых сообщений на устройство.

Мой код тоже делает то, что я хочу, но я обеспокоен потенциальными побочными эффектами, которые могут возникнуть, если я не реализовал его правильно. Я видел по крайней мере одно сообщение на этом форуме, которое указывает, что последовательность: cancellation, dispose и set to null может использоваться для CancellationTokenSource . Но меня беспокоит то, что у меня есть потенциально бесконечный цикл while, который полностью зависит от получения токена отмены. Итак, мой вопрос заключается в том, может ли удаление CancellationTokenSource слишком рано предотвратить токен.IsCanellationRequested не имеет значения true, и если да, то добавит ли добавление задержки какую-либо выгоду?

Ниже приведены соответствующие фрагменты из моего кода:

Глобальные переменные:

 public static bool colorPickerPtrPressed = false;
static CancellationTokenSource cts = null;
static CancellationToken token;
 

События кнопки мыши:

 private void ColorPicker_PtrPressedEvent(object sender, PointerRoutedEventArgs e)
{
    if(cts == null) cts = new CancellationTokenSource();
    token = cts.Token;

    var picker = sender as ColorPicker.ColorPicker;

    colorPickerPtrPressed = true;
    (picker.DataContext as SpheroViewModel).Color = picker.SelectedColor.Color;

    ColorChange(token);

}

private void ColorPicker_PtrReleasedEvent(object sender, PointerRoutedEventArgs e)
{
    if (cts != null)
    {
        cts.Cancel();
        Task.Delay(500).Wait(); // Allow some time for cancel to take effect
        cts.Dispose();
        cts = null;
    } 
}
 

Способы отмены токена:

 public static async Task ColorChange(CancellationToken token)
{
    await Task.Run(() =>
    AllowColorChange(token), token);
}

public static void AllowColorChange(CancellationToken token)
{
        while (!token.IsCancellationRequested)
        {
            colorPickerPtrPressed = true; // Maintain value as true
            Task.Delay(100).Wait(); // allow color updates periodically
        }
    return;
}
 

Ответ №1:

Итак, мой вопрос заключается в том, будет ли задержка между отменой и удалением CancellationTokenSource, как я сделал в «colorpicker_prreleasedevent» ниже, обеспечит ли какую-либо гарантию того, что цикл while завершится?

Нет. Модель отмены является кооперативной, и вызов Cancel просто предоставляет уведомление об отмене, устанавливая IsCancellationRequested свойство всех токенов true равным .

Затем любой отменяемый API, то есть любой метод, который принимает a CancellationToken , должен отслеживать значение этого свойства и отвечать на запрос отмены.

So ColorPicker_PtrReleasedEvent не может гарантировать, что while цикл завершится AllowColorChange .

Комментарии:

1. Вы говорите, что cts. Cancel() всегда будет устанавливать токен. IsCancellationRequested имеет значение true (которое отслеживается циклом while) и что задержка после этого ничего не добавляет?

2. @Stroker347 если вы хотите убедиться, что операция отмены действительно была завершена в отмененном состоянии, ничто не заменит ожидание или (предпочтительно) ожидание этой операции после того, как вы Cancel укажете источник отмены. Использование произвольных задержек — это рецепт плохой производительности и спорадических сбоев приложений.

3. Вы имеете в виду «ожидание cts. Cancel()» или «ожидает разрешения на изменение цвета (токена)»?

4. @Stroker347 await AllowColorChange(token) . Или, что еще лучше await _task , где _task находится кэшированный результат AllowColorChange(token) более раннего вызова. Который должен быть асинхронным методом, возвращающим a Task , и иметь Async суффикс в соответствии с рекомендациями . И лучшее имя в целом. AllowColorChange является ли имя подходящим для bool свойства, а не для метода!

5. На самом деле, я бы знал, если бы цикл while не завершился, потому что изменения цвета продолжали бы отправляться на мое устройство после отпускания кнопки мыши. Однако, если бы я дождался этого и проверил завершение, я мог бы затем повторно выполнить команду Cancel(), если она не была отменена при отпускании кнопки мыши. Тестирование показало, что цикл while завершается по желанию даже без каких-либо задержек, так что, возможно, я зря беспокоюсь.

Ответ №2:

В соответствии с рекомендациями TZ я изменил последние три метода, чтобы ожидать отмененного флага от метода AllowColorChange(token), а затем использовал этот результат как разрешающий для dispose() CancellationTokenResult и установил для него значение null. Если кто-нибудь увидит проблему с тем, что я сделал, пожалуйста, дайте мне знать. Ниже приведен измененный код, который, по-видимому, работает хорошо:

     private void ColorPicker_PntrReleasedEvent(object sender, PointerRoutedEventArgs e)
    {
        if (cts != null)
        {
            cts.Cancel();
            //Task.Delay(200).Wait(); // Allow some time for cancel to take effect
            //cts.Dispose();
            //cts = null;
        }
    }

    public static async Task ColorChange(CancellationToken token)
    {
        bool task = false;
        task = await Task.Run<bool>(() =>
        AllowColorChange(token), token);
        if (task)
        {
            cts.Dispose();
            cts = null;
        }
        else
        {
            // Shouldn't ever reach this point
            bool isBrkPnt = true;
        }
    }

    public static async Task<bool> AllowColorChange(CancellationToken token)
    {
            while (!token.IsCancellationRequested)
            {
                colorPickerPtrPressed = true; // Maintain value as true
                await Task.Delay(100); // allow color updates periodically
            }
        return true; // signal that task was canceled
    }

}
 

Комментарии:

1. Вы уверены, что команды cts.Dispose(); cts = null; удалят отмененный cts , а не новый cts , созданный ColorPicker_PtrPressedEvent обработчиком? Кроме того, вы не передаете token Task.Delay методу, поэтому отмена будет немного отложена (в среднем на ~ 50 мс). Кроме того, стандартная практика в случае отмены заключается в распространении OperationCanceledException значения, а не bool значения. Это означает, что IsCancellationRequested свойство следует использовать редко, и ThrowIfCancellationRequested вместо этого следует использовать метод.

2. Я создаю новый CancellationTokenSource только в том случае, если текущий имеет значение null, поэтому их никогда не должно быть больше одного за раз, поэтому я думаю, что с первым пунктом все в порядке. Я попытался передать токен методу Delay, и это привело к тому, что мой цикл while перестал функционировать так, как я хочу, поэтому мне придется исследовать это дальше. Значение bool было естественным выбором для того, как я использую возвращаемое значение, но я рассмотрю то, что вы предложили в качестве лучшего выбора. Спасибо за ваши предложения, они были очень полезны, и я больше не беспокоюсь, что могу преждевременно избавиться от CancellationTokenSource.

3. TZ, я изменил тело задачи ColorChange(токен) на: «попробуйте{ await Task.Run(async() => {while(true){токен. ThrowIfCancellationRequested(); colorPickerPtrPressed = true; ожидание задачи. Задержка (100, токен);},токен}; } перехват (OperationCanceledException ex), когда (например.CancellationToken == токен){cts. Dispose(); cts = null;}», который работает и исключает использование метода AllowColorChange. Кроме того, передача токена методу Delay теперь работает. Соответствуют ли эти изменения тому, что вы предложили?

4. Да, так намного лучше. Причина, по которой вам нужно устанавливать colorPickerPtrPressed = true каждые 100 мс, мне неясна, и я предполагаю, что это не оптимальное решение для того, что вы пытаетесь сделать, но, вероятно, оно достаточно хорошее. 🙂

5. Каждый раз, когда я отправляю своему устройству команду color, я устанавливаю colorPickerPtrPressed = false в методе, который отправляет команду, чтобы предотвратить дальнейшие изменения при перемещении указателя из элемента управления выбора цвета. Но я также хотел иметь возможность удерживать нажатой кнопку мыши и посылать непрерывный поток, пока не получу нужный цвет. Мне просто не нужно или не хочется слишком перегружать мое устройство командами изменения цвета, когда достаточно периодического обновления. Еще раз спасибо за вашу помощь, это было очень поучительно.

Ответ №3:

После выполнения рекомендаций, предложенных Теодором Зоулиасом, окончательный код теперь выглядит так, как показано ниже. Можно видеть, что между отменой и удалением CancellationTokenSource не используется произвольная задержка, а скорее удаление перемещается в блок catch{}, который запускается OperationCanceledException, возникающим в результате выброса токена.ThrowIfCancellationRequested(); из цикла while(), который был перемещен внутри блока try{}, и его тестовому параметру присвоено значение true. Больше нет необходимости проверять значение токена.IsCancellationRequested запрашивается как параметр цикла while. Это кодирование гарантирует, что удаление не произойдет до тех пор, пока задача в блоке try{} не будет отменена.

      private void ColorPicker_PntrPressedEvent(object sender, PointerRoutedEventArgs e)
    {
        if(cts == null) cts = new CancellationTokenSource();
        token = cts.Token;

        var picker = sender as ColorPicker.ColorPicker;

        colorPickerPtrPressed = true; // True allows new values of color to be sent to device
        (picker.DataContext as SpheroViewModel).Color = picker.SelectedColor.Color;

        ColorChange(token); // Don't await this

    }

    private void ColorPicker_PntrReleasedEvent(object sender, PointerRoutedEventArgs e)
    {
        if (cts != null)
        {
            cts.Cancel();
        }
    }

    public static async Task ColorChange(CancellationToken token)
    {
        try
        {
            await Task.Run(async () =>
            {
                while (true)
                {
                    token.ThrowIfCancellationRequested();
                    colorPickerPtrPressed = true; // Maintain value as true while mouse button remains pressed
                    await Task.Delay(100, token); // allow color updates periodically
                }
            }, token);
        }
        catch (OperationCanceledException ex) when (ex.CancellationToken == token) // includes TaskCanceledException
        {
            if (cts != null) // Shouldn't arrive here if it is null but check just in case
            {
                try
                {
                    cts.Dispose();
                    cts = null;
                }
                catch (ObjectDisposedException e)
                {
                    // Already disposed, do nothing
                    bool brkPnt = true;
                }
            }
        }
    }

}
 

}

Комментарии:

1. Хм, обертывание цикла в a Task.Run усложняет ситуацию, потому что теперь не все будет происходить в одном потоке (потоке пользовательского интерфейса). Теперь возможно, что функция cts.Cancel(); будет вызвана в потоке пользовательского интерфейса сразу после cts.Dispose того, как она была вызвана в фоновом потоке, что приведет к ObjectDisposedException . Я бы предложил удалить Task.Run , поскольку, похоже, он ничего не предлагает. Не о чем беспокоиться, вызывая продолжение в потоке пользовательского интерфейса каждые 100 мс, которое обновляет только bool переменную.

2. Кстати, рассматривали ли вы возможность замены этого механизма на простой System.Windows.Forms.Timer ? Вы можете переключать его Enabled свойство (или вызывать его Start / Stop методы) в соответствующие моменты.

3. ОК. Кажется, что все работает так же хорошо без задачи. Запустите оболочку, как это было с ней.

4. TZ, просто к вашему сведению: после дальнейшего изучения я обнаружил, что с задачей. Присутствующая оболочка запуска, создание, отмена и удаление CancellationTokenSource происходят в потоке пользовательского интерфейса. Только цикл while() выполняется в потоке threadpool. Мне нравится эта конфигурация, потому что «ожидание задачи. Delay()» в цикле while() является необязательным, и если его нет, цикл while() заблокирует все дальнейшее выполнение, если не присутствует оболочка «await Task.Run». Я считаю, что окончательная версия кода, который я опубликовал, хороша. Моим намерением было реализовать асинхронный метод, и я верю, что с вашей помощью мне это удалось.