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

Art Hansen 571 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…

Developer technologies Windows Forms
Developer technologies C#
{count} votes

3 answers

Sort by: Most helpful
  1. Bruce (SqlWork.com) 77,686 Reputation points Volunteer Moderator
    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);
    			}
    
    2 people found this answer helpful.
    0 comments No comments

  2. Bruce (SqlWork.com) 77,686 Reputation points Volunteer Moderator
    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,586 Reputation points Volunteer Moderator
    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


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.