UI freezing due to Task.Run activity in async/await Task.Run() scenario (reworded)

Art Hansen 566 Reputation points
2023-09-12T12:34:08.5533333+00:00

[Previously} Help needed understanding C# await Task.Run() and threading

I’m creating a desktop C# Winforms app.  If an exception is thrown due to a missing essential component the user can choose an automated fix option that searches the app’s archives for a usable replacement.  A progress bar is used .  I’m testing the communication between the UI and  search progress.  For testing purposes there’s  a Winforms timer and a while loop in the Task.Run that completes based on elapsed time or a cancellation flag. Currently the timer’s OnTick event is not firing, progress is not being reported and the UI freezes.   Since I get a boatload of messages from within the while loop that loop is obviously preventing anything else from occurring.

The code:

public partial class Recovery : Form
{
	private int _elapsed;
	private static bool _running;
	private static CustomProgBar _pBar;
	private static System.Windows.Forms.Timer _winFtimer;

    private void InitWinFtimer()
	{
		_winFtimer = new System.Windows.Forms.Timer();
		_winFtimer.Interval = 100;
		_winFtimer.Tick += WinFtimer_OnTick;
		_winFtimer.Start();
	}
	private void WinFtimer_OnTick(object sender, EventArgs e)
	{
		_elapsed += _winFtimer.Interval;
		
		// not happening
		Console.WriteLine($"in OnTick elapsed = {_elapsed}"); 
	}
	private void StartBTN_Click(object sender, EventArgs e)
	{
		InitWinFtimer();
		_pBar.Maximum = 5;

		_running = true;
		IProgress<int> prog = new Progress<int>(value =>
		{
			ActionLBL.Text = value.ToString();
			_pBar.Value = value / 1000;
			
			// not happening
			Console.WriteLine($"in IProgress<int> _pBar.Value = {_pBar.Value}");
		});
		RunTask(prog);
	}
	private async void RunTask(IProgress<int> prog)
	{
		await Task.Run(() =>
		{
			while (_elapsed < 4010 && _running)
			{
				Console.WriteLine($"in while elapsed = {_elapsed}");

				if (prog != null)
					prog.Report(_elapsed);
			}
		});
		ActionLBL.Text = "Completed.";
	}
	private void CancelBTN_Click(object sender, EventArgs e)
	{
		_running = false;
	}
}

My understanding is that the activity in the TasK.Run  occurs on a dedicated thread separate from the UI thread.  I must be missing something fundamental.  Any help will be appreciated…

Windows Forms
Windows Forms
A set of .NET Framework managed libraries for developing graphical user interfaces.
1,871 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,633 questions
{count} votes

3 answers

Sort by: Most helpful
  1. Bruce (SqlWork.com) 61,416 Reputation points
    2023-09-13T17:00:45.9533333+00:00

    your timer is running on the UI, so will only block durning its callback. but your worker thread is 100% cpu bound and updates a UI component continuously, so these updates will block the UI thread.

    if you don't want to use invoke, a common approach is a queue. the background thread inserts entries in a thread safe queue. the UI thread uses a timer to empty and process the queued items. In your case you would need to implement a debounce as there is minimum delay in loading the queue.

    note: the timer uses the event loop to dispatch. but I suspect your sending so many UI update requests, there is no loop.

    if you add a sleep to the background loop, I suspect your code will work.

                while (_elapsed < 4010 && _running)
    			{
    				Console.WriteLine($"in while elapsed = {_elapsed}");
    
                    Thread.Sleep(10); // allow window.loop to run before next UI update
    				if (prog != null)
    					prog.Report(_elapsed);
    			}
    
    1 person found this answer helpful.
    0 comments No comments

  2. Bruce (SqlWork.com) 61,416 Reputation points
    2023-09-12T17:07:58.9466667+00:00

    what messy code. also a background thread can not update a UI control value. the update must be done from the UI thread. if you have a current winform then use invoke. also _running should be a cancelation token.

            private void button1_Click(object sender, EventArgs e)
            {
                _cancellationToken = new CancellationTokenSource();
                _pBar.Maximum = 5;
                IProgress<int> prog = new Progress<int>(value =>
    		    {
    			   ActionLBL.Invoke(() => 
                   { 
                         ActionLBL.Text = value.ToString();
    			         _pBar.Value = value / 1000; 
                   }
    		    });
    
                RunTask(prog, _cancellationToken.Token);
            }
    
            private async void RunTask(prog, CancellationToken token)
            {
                ActionLBL.Text = "Started";
                await Task.Run(() =>
                {
                    for (int i = 0; i < 4010; i = i + 100)
                    {
                        if (token.IsCancellationRequested)
                            return;
    
                        Thread.Sleep(100); // fake work
                        
                        prog?.Report(i);  
                    }
    
                });
                ActionLBL.Text = token.IsCancellationRequested ? "Cancelled" : "Completed";
            }
    
            private void CancelBTN_Click(object sender, EventArgs e)
            {
                _cancellationToken.Cancel();
            }
    
    

  3. Karen Payne MVP 35,386 Reputation points
    2023-09-13T00:51:00.92+00:00

    See my code sample which has a dependency for this class project for Task Dialogs. Both are in the same repository.

    formDemo