0

Below is a simple C# Windows Forms program for .NET Framework 4.6.2.

The program objective is to start an infinite loop by clicking a Start button 1 and then end the loop gracefully by clicking a Stop button 2. If this first loop is not running, I also want the option to start a second infinite loop by clicking Start button 3 and stopping the second loop using Stop button 2.

Here's the Windows form:

Start loop

I have implemented async void to do this. I couldn't find a more elegant way and it seems to work ok. Here's the code:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace CancelLoopTest
{
    public partial class Form1 : Form
    {
        private static CancellationTokenSource myCTS;
        private static CancellationToken myToken;
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            try
            {
                button1.Enabled = false;

                myCTS = new CancellationTokenSource();
                myToken = myCTS.Token;

                while (!myToken.IsCancellationRequested)
                {
                    Console.WriteLine("Loop A running");
                    // Do stuff here that takes about 4.0 seconds
                    await Task.Delay(20);
                }

                Console.WriteLine("Loop A has stopped");
            }
            finally
            {
                button1.Enabled = true;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Console.WriteLine("Stop button clicked");
            myCTS?.Cancel();
        }

        private async void button3_Click(object sender, EventArgs e)
        {
            try
            {
                button3.Enabled = false;

                myCTS = new CancellationTokenSource();
                myToken = myCTS.Token;

                while (!myToken.IsCancellationRequested)
                {
                    Console.WriteLine("Loop B running");
                    // Do stuff here that takes about 0.3s
                    await Task.Delay(20);
                }

                Debug.WriteLine("Loop B has stopped");
            }
            finally
            {
                button3.Enabled = true;
            }
        }
    }
}

My main question: I have used a 20ms delay. In practice, I don't want any delay to hold up the loop. All I want is for the loop to sense whether the Stop button has been pressed. What millisecond value should I put in await Task.Delay(value) to ensure that clicking the Stop button will be detected? Or am I misunderstanding the purpose of await Task.Delay(value)?

Secondary question: This program allows Loop A and Loop B to run concurrently. Both loops are cancelled when Stop is clicked. In practice, I would like to allow only either Loop A or Loop B to run in isolation. Is there an elegant way to prevent Loop B from running if Loop A is already running (and vice-versa)?

Thank you.

DB17
  • 23
  • 6
  • 1
    Do you *really* need a loop? If no, then implement a simple awaitable that uses the CancellationToken to signal its completion and await that instead. If yes, then you could use Task.Yield, or just Task.Delay(0) inside the loop, but this will create a tight loop that might burn cpu cycles, so it depends on whether the loop actually does something useful apart from waiting for the cancellation. – Lasse V. Karlsen Aug 23 '23 at 12:27
  • As for "prevent Loop B from running if Loop A is already running", you need to be more specific. You could use synchronization objects to lock access, so that only one of the loops would run at a time, but this would mean the methods would be called, and entered, and the straggler would be sitting on some sort of wait to get the lock just before the loop. Or, you could disable both buttons whenever one of the methods are called, instead of just the one that was clicked. You need to decide which of the results you want. – Lasse V. Karlsen Aug 23 '23 at 12:29
  • Thanks @LasseV.Karlsen . Do I really need a loop? There may be a different way but my //Do stuff comment is a sequence of commands that needs to be repeated as quickly as possible (it's actually commanding a camera to capture images at a sequence of exposure times). With regards to being specific on "prevent Loop B from running if Loop A is already running", I think that your second suggestion of disabling Start A button if Loop B is running would be fine for my application. Presumably `button1.Enabled = false; button3.Enabled = false;` at the start of each async method would achieve this? – DB17 Aug 23 '23 at 12:44
  • If you want to prevent loop B from running if Loop A is already running, you could just set a flag when starting the loop that will disable the button or will return in button3_Click() before the loop. Also your loops are executed in the main UI thread, so even when started parallel, loops will wait for each other until Task.Delay() is reached. Also your UI will block when executing the long loop. – Mitko Petrov Aug 23 '23 at 12:46
  • @DB17 If you are commanding an IO device, like a camera, you should absolutely do that on a background thread. And use some thread safe method to send the images to the UI thread for display. If you need any delays you may need to either increase the windows timer frequency, or spinwait, depending on the requirements. – JonasH Aug 23 '23 at 12:51
  • @MitkoPetrov "Also your UI will block when executing the long loop". You're right. I tested this by putting a sleep (4000) line in Loop A and a sleep (300) line in Loop B. The output was alternate Loop A running, Loop B running etc. Also the Stop button didn't work any more when I did this! – DB17 Aug 23 '23 at 15:14

3 Answers3

1

What millisecond value should I put in await Task.Delay(value) to ensure that clicking the Stop button will be detected? Or am I misunderstanding the purpose of await Task.Delay(value)?

You first need to understand how Task.Delay and the UI thread works.

The UI thread processes a queue of messages, like "draw", "user moved mouse" or "timer elapsed". If an application stop processing messages, it stops responding, and windows will show the "application not responding message".

Task.Delay is a wrapper around a timer, essentially asking the OS, "at a time 20ms in the future, please send me a message". And the magic of async/await will make make the UI thread continue execution after Task.Delay. Also note that Task.Delay, and all other timers, will have a resolution of ~16ms by default.

If you want to skip the delay you could do something similar with:

private void LoopMethod(){
   if((!myToken.IsCancellationRequested)){
        this.BeginInvoke(LoopMethod);
    }
}

While this look very different, it will basically do the same thing, except without the delay. While this should not completely hang your program, it will likely become highly unresponsive, so it is really not something I would recommend.

An alternative would be to run a loop on a background thread. That way you do not have to worry about the UI thread and message processing. But you do need to worry about thread safety.

In the end there is no telling what you should do, since you do not describe any actual purpose. If you want to do something periodically you should probably use one of the timers. If you want to run something compute intensive you should probably use a background thread, see Task.Run.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Thank you @jonash. That's a very good and patient explanation! Since a delay of 16ms does not have much impact on my application, I will probably keep a small time value in Task.Delay. You mentioned implementing a background thread to control an IO device. In my case, the program is doing nothing else (e.g. no progress bar, no drawing update) while the code communicates through the camera SDK. The camera SDK controls the image file saving. The code doesn't appear to have a problem running on the UI thread. Is it bad practice to do what I'm doing? Appreciate your thoughts. – DB17 Aug 23 '23 at 15:02
  • @DB17 It depends om what you are doing and what the requirements are. Using a background thread allow tighter control, but require you to consider thread safety. If you are not working at high frame rates, have tight timing tolerances, and are not making any blocking calls to the SDK, then using the UI thread should be fine. Many camera SDKs have blocking calls to receive images, but that might not be a concern if the SDK does the saving. – JonasH Aug 23 '23 at 15:18
  • Thanks again! I have marked this as the answer due to your explanation of how Task.Delay and the UI thread works – DB17 Aug 23 '23 at 15:40
1

All I want is for the loop to sense whether the Stop button has been pressed.

Just replace the loop:

while (!myToken.IsCancellationRequested)
{
    await Task.Delay(20);
}

...with this line:

await Task.WhenAny(Task.Delay(Timeout.Infinite, myToken));

The Task.Delay(Timeout.Infinite, myToken) returns a Task that will never complete, unless the myToken is canceled. When this happens it will complete as Canceled, which means that if you await it directly it will throw an OperationCanceledException. You can either try/catch and suppress the exception, or wrap it in a Task.WhenAny to achieve the same effect.

Is there an elegant way to prevent Loop B from running if Loop A is already running (and vice-versa)?

I don't know about elegant, but the simplest is to disable both buttons when either one is clicked:

try
{
    button1.Enabled = false;
    button2.Enabled = false;
    //...
}
finally
{
    button1.Enabled = true;
    button2.Enabled = true;
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    Thank you! I will implement your suggestion to disable both buttons. Thanks for taking the time to post this. – DB17 Aug 23 '23 at 15:05
-1

I think using BackgroundWorkes is more suitable for your case and is a better & elegant way:

public partial class Form1 : Form
{
    BackgroundWorker worker1;
    BackgroundWorker worker2;
    public Form1()
    {
        InitializeComponent();
        worker1 = new BackgroundWorker();
        worker1.DoWork += Worker1_DoWork;

        worker2 = new BackgroundWorker();
        worker2.DoWork += Worker2_DoWork;

        worker1.WorkerSupportsCancellation = true;
        worker2.WorkerSupportsCancellation = true;
    }

    private void Worker2_DoWork(object? sender, DoWorkEventArgs e)
    {
        while (!worker2.CancellationPending)
        {
            Debug.WriteLine("Worker 2");
        }
    }

    private void Worker1_DoWork(object? sender, DoWorkEventArgs e)
    {
        while (!worker1.CancellationPending)
        {
            Debug.WriteLine("Worker 1");
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        worker1.RunWorkerAsync();
    }

    private void button2_Click(object sender, EventArgs e)
    {
        if (worker1.IsBusy)
        {
            worker1.CancelAsync();
            Debug.WriteLine("Woker 1 stoped.");
        }

        if (worker2.IsBusy)
        {
            worker2.CancelAsync();
            Debug.WriteLine("Woker 2 stoped.");
        }
    }

    private void button3_Click(object sender, EventArgs e)
    {
        worker2.RunWorkerAsync();
    }
}

Further more you can have implementation for when the loop has ended: worker.RunWorkerCompleted += Worker2_RunWorkerCompleted;. This is fired after cancelation has been requested.

Radu Hatos
  • 351
  • 1
  • 6
  • 1
    `BackgroundWorker`s [are so pre-2012](https://stackoverflow.com/questions/12414601/async-await-vs-backgroundworker/64620920#64620920 "Async/await vs BackgroundWorker"). – Theodor Zoulias Aug 23 '23 at 13:30
  • 1
    Thank you @radu-hatos. I did consider a background worker initially but was swayed towards async/await byhat appears to be a gathering consensus on this for .NET 4.5 and above, e.g. https://halfblood.pro/how-to-replace-backgroundworker-with-async-await-and-tasks-80d7c8ed89dc – DB17 Aug 23 '23 at 14:47