We have all heard that it's important to keep the UI thread responsive, so we implement async/await everwhere. I'm building a text editor, where 'everything' is asynchron. However, now I find that its subject to race conditions on the UI thread when code runs before other code has finished. Of course thats the whole idea of 'responsive UI thread', that it can run code, while it's awating other code. I need some code to wait for other code to finish before it runs. I have boiled the problem down to this code where I simply process keystrokes:
private async void Form1_KeyPress(object sender, KeyPressEventArgs e)
{
//Wait until it's your turn (await HandleKey has returned) before proceeding
await HandleKey(e.KeyChar);
}
async Task HandleKey(char ch)
{
await GetCaretPosition();
Point newPosition = await Task.Delay(1000);
await SetCaretPosition(newPosition);
}
As you can see when the first key is processed (awating) the next key can start to process. In this simple case the second key handling code will get an old value of caretposition, because the first key handling has not yet updated caretposition. How can I make the code in KeyPress event wait until the first key has finished processing? Going back to synchron coding is not an option.
For anyone browsing this: This is what I came up with, based on k1dev's link about SemaphoreSlim
(read about it here.
It's actually suprisingly easy. Based on the code in the question, I add a SemaphoreSlim
and Waits on it (asynchron). A SemaphoreSlim
is a lightweight Semaphore
especially made for wating on the UI thread. I use a Queue<char>
to make sure keys are handled in the right order:
SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
Queue<char> queue = new Queue<char>();
private async void Form1_KeyPress(object sender, KeyPressEventArgs e)
{
queue.EnQueue(e.KeyChar);
await semaphore.WaitAsync();
try
{
await HandleKey(queue.DeQueue());
}
finally
{
semaphore.Release();
}
}
As a bonus: I have a method that I simply want to skip if the app is busy, that can be done using this code:
if (await semaphore.WaitAsync(0)) //Note the zero as timeout
{
try
{
await MyMethod();
}
finally
{
semaphore.Release();
}
}