Как обеспечить, чтобы одна активная задача всегда уничтожалась и заменялась при каждом вызове метода, запускающего Task.Run? - PullRequest
0 голосов
/ 22 декабря 2018

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

При отладке и вызове метода (быстро), скажем, 5 раз, я вижу очень странные результаты.
Например: 2-я задача вызывающих абонентов запущена, а последующие задачи вызывающих абонентов отменены (завершены)).

Ожидаемое поведение для выполнения Задачи последнего вызывающего абонента (5-й вызывающий) и отмены (прекращения) всех предыдущих вызывающих Задач.

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

public static class NotificationsHelper
{
    private static CancellationTokenSource _cts = new CancellationTokenSource();   

    // Set Future Notification (From outside this class).
    // If called multiple times, the existing task should be killed and a new task replaces it.
    public static void SetFutureNotification(string notificationText, DateTime notificationDateTime, Action<string> notificationAction)
    {

        CancelNotification();
        _cts = new CancellationTokenSource();

        Task.Run(async () =>
        {

            while (!_cts.Token.IsCancellationRequested)
            {
                await Task.Delay(1000, _cts.Token);

                if (DateTime.Now > notificationDateTime)
                {
                    notificationAction?.Invoke(notificationText);
                    _cts.Cancel();
                }

            }

        }, _cts.Token);

    }

    // Cancel Active Future Notification (From outside this class).
    public static void CancelNotification()
    {
        if (_cts != null && _cts.Token != null && _cts.Token.CanBeCanceled == true)
        {
            _cts.Cancel();
        }
    }
}

Редактировать: я переформатировал свой код, чтобы проверить предложенный Олегом ответ (ниже), добавив Id для отслеживания задач.Это подтвердило желаемый результат:

public static class NotificationsHelper
{
    private static int _counter = 0;
    private static CancellationTokenSource _cts;

    // Set Future Notification (From Anywhere).
    // If called multiple times, the existing task should be killed and a new task replaces it.
    public static void SetFutureNotification(string notificationText, DateTime notificationDateTime, Action<string> notificationAction)
    {
        _counter += 1;
        var id = _counter.ToString();
        Console.WriteLine("Method Called: " + id);

        CancelNotification();
        _cts = new CancellationTokenSource();
        var cts = _cts; // I'm local cts and will be captured by one task only

        Task.Run(async () =>
        {

            while (!cts.Token.IsCancellationRequested)
            {
                await Task.Delay(1000, cts.Token);

                if (DateTime.Now > notificationDateTime)
                {
                    notificationAction?.Invoke(notificationText);
                    cts.Cancel();
                }

                Console.WriteLine("Task active: " + id);

            }

        }, cts.Token).ContinueWith(t => { Console.WriteLine("Task exited: " + id); });

    }

    // Cancel Notification (From Anywhere).
    public static void CancelNotification()
    {
        if (_cts != null && _cts.Token != null && _cts.Token.CanBeCanceled == true)
        {
            _cts.Cancel();
        }
    }
}

Ответы [ 2 ]

0 голосов
/ 22 декабря 2018

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

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

// When starting the task:
_currentTask = Task.Run(async () => ...);


// in Cancel method:
if (_cts != null && _cts.Token != null && _cts.Token.CanBeCanceled == true)
{
        _cts.Cancel();
        // Wait for the background task to finish.
        // Maybe with a try/catch block around it, because it might throw a
        // Cancellation exception
        await _currentTask;
        _cts = null;
        _currentTask = null;
}

Это должно работать, если вы всегда запускаете и пытаетесь отменить фоновые задачи из одного потока (например, потока пользовательского интерфейса).Если эти операции выполняются из разных потоков, вам также может потребоваться защитить _cts и _currentTask, например, с помощью ожидаемого Mutex.

Если вам не важно, выполняется ли фоновая задача, ипросто хотите, чтобы он закончился в какой-то момент времени, тогда подход, изложенный Олегом Богдановым с захватом текущего CancellationToken в фоновом режиме, также будет работать.

0 голосов
/ 22 декабря 2018

Похоже, ваше намерение состояло в том, чтобы отменить каждую задачу перед запуском новой, и это было сделано почти правильно - проблема в том, что каждая задача захватывает и проверяет одну и ту же переменную-член _cts, которую разделяют все задачи.Несмотря на то, что вы new делаете это для каждого уведомления, после вашей задержки (1000) все они знают только о последнем, которое вы создали.Вам нужно, чтобы у каждой задачи была своя копия токена отмены, которую вы будете отменять при каждой последующей задаче:

  public static void SetFutureNotification(string notificationText, DateTime notificationDateTime, Action<string> notificationAction)
{

    CancelNotification();
    _cts = new CancellationTokenSource();
    var cts = _cts; // I'm local cts and will be captured by one task only

    Task.Run(async () =>
    {

        while (!cts.Token.IsCancellationRequested)
        {
            await Task.Delay(1000, cts.Token);

            if (DateTime.Now > notificationDateTime)
            {
                notificationAction?.Invoke(notificationText);
                cts.Cancel();
            }

        }

    }, cts.Token);

}

Теперь ваша процедура отмены (которая не требует изменений) будетотменять последнее созданное задание, и только последнее задание узнает об этом

...