4

In a GUI application, there are a ton of ways to run asynchronous code and have the result run on your "main" thread:

  • Use async/await
  • Use a BackgroundWorker
  • someControl.Invoke()
  • .ContinueWith(..., TaskScheduler.FromCurrentSynchronizationContext())

etc. etc.

However, none of these options work in a console application because there is no synchronization context or GUI run-loop.

I'm modifying a console project which has its own manual run-loop. Something like this:

while (!cancelled)
{
    if(api.HasMessage())
    {
        api.HandleMessage();
    }
    Thread.Sleep(1);
}

I'd like to be able to make asynchronous web-requests in this project, while still handling the results on the main run-loop thread to avoid typical multi-threading issues. Of course I could do this manually using queues, but it would be nice to be able to use async/await or one of the other paradigms which handles this much nicer.

Is there a method I can call from the run-loop to make this possible? I'm imagining something like, say, Thread.ResolveCurrentAwaitingTasks().

BlueRaja - Danny Pflughoeft
  • 84,206
  • 33
  • 197
  • 283
  • You are writing this ugly loop just because of *there is no synchronization context* ? Not clear what you really want to do... – Eser Feb 27 '16 at 22:59
  • @Eser: I have not written anything, that is the way the project is set up. I'm asking if it's possible to use the common convenient multi-threaded paradigms in this situation. I'll admit to not fully understanding what a synchronization context actually is, so maybe it's possible for me to create one, and eliminate this run loop altogether? Of course `api.HasMessage()` and `api.HandleMessage()` still need to get called regularly, somehow. – BlueRaja - Danny Pflughoeft Feb 27 '16 at 23:01
  • 2
    You are doomed to re-invent Application.Run(), why don't you just use it? The Winforms flavor works fine in a console app and doesn't need a window to get the job done and give you the SynchronizationContext you want. The usual causality however is that your simple console app main thread suddenly isn't simple anymore. Just like your own version, Application.Run is a blocking call. It has to be. – Hans Passant Feb 27 '16 at 23:35
  • @HansPassant: Correct me if I'm wrong, but doesn't `Application.Run()` just create a run-loop to handle Windows-messages? I'm not interested in Windows-messages, I need to handle messages originating from this library _(specifically, "this project" is JesseCar69's `SteamBot` and the "library" is an older version of `SteamKit`)_ – BlueRaja - Danny Pflughoeft Feb 27 '16 at 23:49
  • No, it solves the [producer-consumer problem](https://en.wikipedia.org/wiki/Producer%E2%80%93consumer_problem). Like any dispatcher loop does. So does yours, you just forgot to add the Send and Post methods. That it happens to know how to dispatch Windows messages is just a minor implementation detail and you just don't care, you won't use any yourself and there is no window. – Hans Passant Feb 28 '16 at 00:00

1 Answers1

5

It's not as easy as just calling a method, because async continuations are scheduled to a SynchronizationContext or TaskScheduler, not a thread. So, you'd have to write one of those - not a very fun prospect.

You can use a SynchronizationContext that I wrote called AsyncContext; normal usage looks like this:

AsyncContext.Run(() =>
{
  while (!cancelled)
  {
    if(api.hasMessage())
    {
      api.handleMessage();
    }
    Thread.Sleep(1);
  }
});

and this works fine with async code:

AsyncContext.Run(async () =>
{
  while (!cancelled)
  {
    if(api.hasMessage())
    {
      await api.handleMessageAsync();
    }
    Thread.Sleep(1);
  }
});

Though I must say that the Thread.Sleep is a bit hackish. I'd consider using events or some other kind of message arrival notification like await api.GetNextMessageAsync().

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Where can I read about how this works - the call to `Thread.Sleep()` somehow(?) lets the `SynchronizationContext` know it can now run any awaiting tasks? Also I did not write the API. It does have a `WaitForMessage()` method, but I don't know if that still allows the `SynchronizationContext` to magically run its awaiting tasks, as well. – BlueRaja - Danny Pflughoeft Feb 27 '16 at 23:05
  • No. Both `Thread.Sleep` and `WaitForMessage` would block the currently-running thread. It would only be able to process other tasks at an `await` point. E.g., `await Task.Delay(1)`. More on `SynchronizationContext` [here](https://msdn.microsoft.com/magazine/gg598924.aspx). – Stephen Cleary Feb 27 '16 at 23:07
  • So your first example would not actually work? The API is not awaitable _(and I cannot change it)_, so the second example is not an option. Unless I can `await DoNothing()` to have it run any awaiting tasks? – BlueRaja - Danny Pflughoeft Feb 27 '16 at 23:09
  • 1
    You could; there is an `await Task.Yield();` you can do. But I would suggest changing the api. – Stephen Cleary Feb 27 '16 at 23:11
  • Hmm.. actually I've found that async/await works with the code I've posted without a `Task.Yield()` call. Now I'm even more confused. – BlueRaja - Danny Pflughoeft Mar 03 '16 at 23:01
  • 1
    @BlueRaja-DannyPflughoeft: Something else may have a nested message loop. This is not uncommon if you're working with STA objects (including UI components). – Stephen Cleary Mar 04 '16 at 01:08