c# как работает скоординированная отмена?

Во первых не понимаю почему поведение кода снизу отличается в Debug и Release:

  • в Debug задача выводит числа до 10 и после отменяется,
  • в Release она отменяется сразу и числа не успевают вывестись (хотя отмена вроде как называется скоординированной, но задача завершается без участия кода внутри задачи).

Но при этом, если в Release подождать между запуском и отменой (или вызвать CancelAfter(1000) вместо Cancel()), то задача будет выполняться так же как в Debug, пока ее не отменят изнутри

public class Program
{
    public static void Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        Task task = Task.Run(() => Print(cts.Token), cts.Token);
        //отменяется сразу в Release
        cts.Cancel();
        Console.ReadKey();
    }
    static void Print(CancellationToken token)
    {
        int i = 0;
        while (true)
        {
            i++;
            Thread.Sleep(1000);
            Console.WriteLine(i);
            if (i == 10) token.ThrowIfCancellationRequested();
        }
    }
}

Ответы (1 шт):

Автор решения: CrazyElf

Можно, конечно, начать гадать, почему поведение различается под дебагом и релизом, но главное тут то, что вы неправильно используете асинхронность, а в этом случае поведение может быть какое угодно не предсказуемое.

"Вылечить" ваш код довольно просто, переведя его полностью в асинхронную версию:

static async Task Print(CancellationToken token)
       ^^^^^ ^^^^
{
    int i = 0;
    while (true)
    {
        i++;
        await Task.Delay(1000, token);
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        Console.WriteLine(i);
        if (i == 10) token.ThrowIfCancellationRequested();
    }
}

В этом коде отмена происходит, естественно, моментально, как и задумано авторами .NET Core. А когда вы используете void метод и внутри него усыпляете поток, не используя асинхронность - да бог весть как это должно себя вести, да как угодно вообще, ибо асинхронность и многопоточность - это две разные концепции, они не обязаны дружить предсказуемым образом, когда они используются не в рамках парадигмы асинхронности, а каждая сама по себе.

P.S. Могу только предположить, что в Release по каким-то причинам старт таски происходит чуть позже, cts.Cancel(); успевает отработать раньше, чем Task.Run проверит токен на кансельнутость перед тем, как реально запустить таску. Почему так - нужно смотреть сгенерированный код. А в Debug наоборот - таска стартует, а уже после этого отрабатывает cts.Cancel();, но он уже не может повлиять на уже запущенную таску, поскольку в ней вызывается void метод, в котором нет никаких await, он просто спокойно выполняется в своём потоке, никак не взаимодействуя с механизмом асинхронности и его средствами передачи сигналов. Но, конечно, явная проверка token.ThrowIfCancellationRequested(); при этом срабатывает.

→ Ссылка