0

I'm running a foreach and I would like to cancel its execution on a key press.

While I succeeded doing it integrating a single if (keypress) within the loop, now I'm trying to achieve the same using a CancellationToken while the task for listening for a key stroke is running.

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;
Task.Factory.StartNew(() =>
{
    while (true)
    {
        foreach (var station in stations)
        {
            /*if (Console.KeyAvailable)
            {
                break;
            }*/
            Console.WriteLine(station.name + " ");
            Thread.Sleep(100);
        }
        Thread.Sleep(100);
        if (ct.IsCancellationRequested)
        {
                    // another thread decided to cancel
                    Console.WriteLine("task canceled");
            break;
        }
    }
}, ct);
ts.Cancel();
Console.ReadLine();

I came from this answer How do I abort/cancel TPL Tasks? which helped me a lot. However, while it works without the foreach, right now the foreach has to end before the task is cancelled. Obviously, it looks like the iteration has to end before proceeding to the next step and what I don't understand is how I can make the foreach stop.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
Roberto Rizzi
  • 1,525
  • 5
  • 26
  • 39
  • https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.throwifcancellationrequested?view=net-5.0 – ProgrammingLlama Sep 04 '21 at 10:01
  • 2
    So you want to stop the `foreach` loop in a non-cooperative fashion? I don't think that the `CancellationToken` can help you with that. Cancellation in .NET [is cooperative](https://learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads). – Theodor Zoulias Sep 04 '21 at 10:09
  • I think your are trying to achieve a event driven approach. You even tried to create a even loop inside your task. But Event Loops should be the top structure in an event driven application and all the other stuff should be put inside a loop asynchronously (like a Task). Try move your while loop out of your Task... The loop should be running in foreground to detect your events and the task in background – Douglas Ferreira Sep 04 '21 at 12:09

3 Answers3

3

Cancellation is co-operative.

You need to check inside the foreach() and at any other appropriate point in your routines, to see if cancellation has been requested.

If it has, then you could prematurely exit out of the foreach(), (if your logic allows it), while ensuring that you clean up any resources and complete any actions that need to be completed.

Also, I suggest, instead of Thread.Sleep(), use await Task.Delay(100);.

Rowan Smith
  • 1,815
  • 15
  • 29
1

It seems as though it's as simple as repeating your if:

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;
Task.Factory.StartNew(() =>
{
    while (true)
    {
        foreach (var station in stations)
        {
            /*if (Console.KeyAvailable)
            {
                break;
            }*/
            if (ct.IsCancellationRequested)
            {
                // another thread decided to cancel
                break;
            }
            Console.WriteLine(station.name + " ");
            Thread.Sleep(100);
        }
        Thread.Sleep(100);
        if (ct.IsCancellationRequested)
        {
                    // another thread decided to cancel
                    Console.WriteLine("task canceled");
            break;
        }
    }
}, ct);
ts.Cancel();
Console.ReadLine();
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
0

As I commented before, I think you're trying to achieve something like this:

    public class Program
{
    static ConcurrentQueue<int> queue = new(); // >= .NET 5 / C# 9 only

    public static void Main(string[] args)
    {
        var ts = new CancellationTokenSource();
        var rand = new Random();
        Task activeTask = null;

        while (true)
        {
            var keyPressed = Console.ReadKey(true).Key;

            if (keyPressed == ConsoleKey.Q)
            {
                for (int i = 0; i < 1000; i++)
                    queue.Enqueue(rand.Next(0, 1000));

                Console.WriteLine("Random elements enqueued");
            }

            if (keyPressed == ConsoleKey.D)
            {
                if(activeTask == null)
                {
                    ts.Dispose();
                    ts = new CancellationTokenSource();

                    CancellationToken ct = ts.Token;
                    activeTask = BackgroundLoopAction(ct);
                }
                else
                    Console.WriteLine("Background loop task already running");
            }

            if (keyPressed == ConsoleKey.X)
            {
                ts.Cancel();
                activeTask = null;
            }
                

            if (keyPressed == ConsoleKey.Escape)
                break;
        }
    }

    private static Task BackgroundLoopAction(CancellationToken ct)
    {
        return Task.Run(() =>
        {
            while (queue.Count > 0)
            {
                queue.TryDequeue(out int q);
                Console.WriteLine($"Dequeued element: {q}");
                Thread.Sleep(100);

                if (ct.IsCancellationRequested)
                    break;
            }

            Console.WriteLine(ct.IsCancellationRequested ? "Task canceled by user event" : "Task completed");
        }, ct);
    }
}

App Output

Douglas Ferreira
  • 434
  • 1
  • 7
  • 23