4

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.

Poul Bak
  • 10,450
  • 5
  • 32
  • 57
  • 1
    https://blog.cdemi.io/async-waiting-inside-c-sharp-locks/ take a look at this – k1dev Aug 09 '18 at 13:35
  • 2
    That is the dirty little secret of async/await, it brings back all the evil of re-entrancy bugs that Application.DoEvents() produces as well. The example is too synthetic to propose a practical solution, but a Queue could solve it. Bummer when the user types fast, you have to do something about that as well. I bet your next project is going to look different :) – Hans Passant Aug 09 '18 at 13:44
  • @k1dev, I wish I could give you more than 1 point, because that IS the answer! – Poul Bak Aug 13 '18 at 19:55

2 Answers2

0

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();
    }
}
Poul Bak
  • 10,450
  • 5
  • 32
  • 57
-1

First, it sounds like there is more to this question or that there are likely to be follow-ups.

That said, you may try to make your HandleKey synchronized to manage shared resources:

[MethodImpl(MethodImplOptions.Synchronized)]
async Task HandleKey(char ch)
{

Try this thread for reference: C# version of java's synchronized keyword?

PS - So the more I think about this the more it seems like your trying to perform some unit of work synchronously but offload it off the ui thread. That said, synchronizing the HandleKey method won't achieve the desired result. I think you may be looking for a Dispatcher pattern:

https://www.what-could-possibly-go-wrong.com/the-dispatcher-pattern/

https://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher(v=vs.110).aspx

  • Will this make sure that they're processed in the right order (so 'abc' doesn't become 'acb' ? – Poul Bak Aug 09 '18 at 13:39
  • 1
    @PoulBak please see updated answer. Await blocks so technically yes it will process events in order as they come up in the message queue. However, I suspect what you are trying to accomplish is synchronized execution off the ui thread (i.e. you simply want to dispatch your events to a worker). Please look at the dispatcher pattern. – Alexander Toptygin Aug 09 '18 at 13:43
  • 2
    [CS4015] 'MethodImplOptions.Synchronized' cannot be applied to an async method – activout.se Jan 24 '19 at 15:17