Run Async Form in Background without deadlocking
FormA (see example below) is old and loads slowly due to lots of lazy-loading EF6 code etc. We've managed to speed it up with some async methods that allow the form to at least be displayed whilst loading data. Async all the way up to the event handlers is working nicely - UNTIL: the same form is loaded in the background by a second form (FormB) that loops through and loads the form for multiple records in order to run some checks over the data as it loads (yeah not at all ideal, but not going there now).
Before the async changes, FormA was being created in a method on FormB started using Thread.Start(...) - now we've reworked to be async all the way up to the Button_Click event handler that started the mass update process, but FormA is now hogging the UI thread so FormB never updates any progress indicators until everything is finished.
We've tried using Task.Run(...) to create instances of FormA and do its work, but we always get a deadlock on the first await asyncMethod(...) in FormA. The lowest level of those async methods do use ConfigreAwait(False), but the upper levels don't because there are UI Components that need to be updated after retrieving data from the database which must happen on the same thread that created the form and its controls.
FormA has this sort of code
public partial class FormA : Form
{
private int _counter;
public FormA(int counter)
{
InitializeComponent();
_counter = counter;
}
private async void formDoWork_Load(object sender, EventArgs e)
{
this.Text = "Loading...";
await Task.Delay(5000); // Various async tasks happen here
this.Text = "Loaded";
}
public async Task LoadDataAsync()
{
this.workingLabel.Text = "Working...";
string result = await GetDataAsync(); // Get the data but can't ConfigureAwait(false) because we need to update UI components afterwards
this.workingLabel.Text = "Finished! " + result;
}
private async Task<string> GetDataAsync()
{
// Get the data from the database - we can ConfigureAwait(false) here because no UI components are updated at this level (in real code this would be in a separate data layer library)
await Task.Delay(3000).ConfigureAwait(false);
return $"Loading #{_counter}";
}
}
Then in FormB we do something like this:
public partial class FormB : Form
{
public FormB()
{
InitializeComponent();
}
private async void GoButton_Click(object sender, EventArgs e)
{
this.GoButton.Enabled = false;
var activeTasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
// Start processing form asynchronously and keep track of the task for later
Task processTask = ProcessFormAsync(i);
activeTasks.Add(processTask);
this.ProgressLabel.Text = $"Started #{i}";
await Task.Delay(1000);
}
// Wait for all the tasks to be finished, then update progress on UI
await Task.WhenAll(activeTasks);
this.ProgressLabel.Text = "Finished.";
this.GoButton.Enabled = true;
}
private async Task ProcessFormAsync(int counter)
{
using (var newForm = new FormA(counter))
{
// Show the form to trigger form_Load event handler and then hide it again
newForm.Show();
newForm.Hide();
// Now call the main Load function which is also async.
await newForm.LoadDataAsync();
newForm.Close();
}
}
}
And all works well, except that everything (besides the continuation of FormA.GetDataAsync) is running on the Main UI thread
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 0 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 0 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 0 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 1 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 1 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 1 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 2 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 2 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 2 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 0 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 0 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 3 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 3 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 3 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 1 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 1 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 4 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 4 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 4 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 0 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 2 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 2 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 5 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 5 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 5 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 1 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 3 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 3 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 6 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 6 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 6 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 2 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 4 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 4 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 7 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 7 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 7 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 3 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 5 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 5 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 8 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 8 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 8 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 4 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 6 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 6 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 9 >>>
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 9 >>>
AsyncWinForms.FormA.GetDataAsync() | Main Thread(7244) | 9 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 5 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 7 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 7 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 6 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 8 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 8 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 7 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13984) | 9 <<<
AsyncWinForms.FormA.LoadDataAsync() | Main Thread(7244) | 9 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 8 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Main Thread(7244) | 9 <<<
The program '[17808] AsyncWinForms.exe' has exited with code 0 (0x0).
In our real scenario FormA is so busy doing UI work it means that nothing on FormB is updated until the end of the processing which defeats the purpose of updating progress. So we've tried to change it to run FormA on a bacgkround thread like this:
Task processTask = Task.Run(async () => await ProcessForm(i).ConfigureAwait(false)));
And FormB now gets to update progress, but FormA deadlocks on any await ...Async method calls.
Deadlocking Output:
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(13184) | 0 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(13184) | 0 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 0 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(13184) | 1 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(13184) | 1 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 1 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(13184) | 2 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(13184) | 2 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 2 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(13184) | 3 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(13184) | 3 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 3 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 0 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(13184) | 4 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(13184) | 4 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 4 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(12084) | 1 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(13184) | 5 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(13184) | 5 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 5 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(12084) | 2 <<<
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(13184) | 6 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(13184) | 6 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(12084) | 3 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 6 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(11596) | 7 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(11596) | 7 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 4 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(11596) | 7 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(13184) | 8 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(13184) | 8 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(12084) | 5 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 8 >>>
AsyncWinForms.FormA.formDoWork_Load(object, System.EventArgs) | Worker Thread(11596) | 9 >>>
AsyncWinForms.FormA.LoadDataAsync() | Worker Thread(11596) | 9 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(13184) | 6 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(11596) | 9 >>>
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(11596) | 7 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(1548) | 8 <<<
AsyncWinForms.FormA.GetDataAsync() | Worker Thread(12084) | 9 <<<
The thread 0x60c has exited with code 0 (0x0).
The thread 0x2f34 has exited with code 0 (0x0).
The thread 0x2d4c has exited with code 0 (0x0).
The thread 0x3380 has exited with code 0 (0x0).
The program '[6036] AsyncWinForms.exe' has exited with code 0 (0x0).
Why is this happening? We can't see anything that is blocking the new worker threads, but it deadlocks everytime. What alternatives do we have?