6

Let's say I have a method fooCPU that runs synchronously (it doesn't call pure async methods performing I/O, or use other threads to run its code by calling Task.Run or similar ways). That method performs some heavy calculations - it's CPU bound.

Now I call fooCPU in my program without delegating it to be executed by a worker thread. If one line of fooCPU will take long to run, no other lines will be executed until it finishes. So for example, calling it from the UI thread causes the UI thread to freeze (GUI will become unresponsive).

When I stated that async/await is an imitation of mutlithreading. The lines of two different pieces of code are executed in turns, on a single thread. If one of these lines will take long to run, no other lines will be executed until it finishes.,

I've been told that it's true for async used on the UI thread, but it's not true for all other cases (ASP.NET, async on the thread pool, console apps, etc).

Could anyone tell me what this might mean? How is UI thread different from the main thread of a console program?

I think nobody wants anyone here on this forum to continue the discussion of related topics, as they appear in the comments for instance, so it's better to ask a new question.

user4205580
  • 564
  • 5
  • 25
  • I would compile the same code on a win forms app and a console app and would check the differences of the compiled IL using ildasm or reflector to see for myself. – Oguz Ozgul Nov 15 '15 at 22:57
  • 1
    @OguzOzgul That would be useful with many issues, but not this one - the code is exactly the same. What changes is the global state - the presence of a synchronization context. – Luaan Nov 15 '15 at 23:14
  • 1
    The answers are pretty good but the only thing that will satisfy your curiosity is understand what await does in detail. The search ".net await internals" looks pretty good. If you've got an hour to spent this will answer everything, I hope. – usr Nov 15 '15 at 23:27
  • Look what [this in the threads section says](https://msdn.microsoft.com/en-us/library/hh191443.aspx) says. I think this is because when an async task is created in a UI thread, the synchronization context used to call back on the UI thread is always retrieved by a call to [this](https://msdn.microsoft.com/en-us/library/system.threading.tasks.taskscheduler.fromcurrentsynchronizationcontext(v=vs.110).aspx). I will confirm this soon. Since your method is always active (does not have a non-blocking io operation), it uses the UI thread through it's execution. – Oguz Ozgul Nov 16 '15 at 07:08
  • Seems like, the UI thread is no different then the main thread of a console application. A cpu bound async operation, awaited with the await command on a console main thread, still runs on that same main thread and blocks the console application the same way it blocks (freezes) the UI. I think it was a misleading comment that said things are different on aconsole application or an Asp.Net worker thread. Since your async method does nothing async for real, there is nothing awaitable, and execution never returns to the initiating thread, does'nt matter a Forms or a Console or web application – Oguz Ozgul Nov 16 '15 at 09:19

3 Answers3

10

I recommend you read my async intro post; it explains how the async and await keywords work. Then, if you're interested in writing asynchronous code, continue with my async best practices article.

The relevant parts of the intro post:

The beginning of an async method is executed just like any other method. That is, it runs synchronously until it hits an “await” (or throws an exception).

So this is why the inner method in your console code example (without an await) was running synchronously.

Await examines that awaitable to see if it has already completed; if the awaitable has already completed, then the method just continues running (synchronously, just like a regular method).

So this is why the outer method in your console code example (that was awaiting the inner method which was synchronous) was running synchronously.

Later on, when the awaitable completes, it will execute the remainder of the async method. If you’re awaiting a built-in awaitable (such as a task), then the remainder of the async method will execute on a “context” that was captured before the “await” returned.

This "context" is SynchronizationContext.Current unless it is null, in which case it is TaskScheduler.Current. Or, the simpler version:

What exactly is that “context”? Simple answer:

  1. If you’re on a UI thread, then it’s a UI context.
  2. If you’re responding to an ASP.NET request, then it’s an ASP.NET request context.
  3. Otherwise, it’s usually a thread pool context.

Putting all of this together, you can visualize async/await as working like this: the method is split into several "chunks", with each await acting as a point where the method is split. The first chunk is always run synchronously, and at each split point it may continue either synchronously or asynchronously. If it continues asynchronously, then it will continue in a captured context (by default). UI threads provide a context that will execute the next chunk on the UI thread.

So, to answer this question, the special thing about UI threads is that they provide a SynchronizationContext that queues work back to that same UI thread.

I think nobody wants anyone here on this forum to continue the discussion of related topics, as they appear in the comments for instance, so it's better to ask a new question.

Well, Stack Overflow is specifically not intended to be a forum; it's a Question & Answer site. So it's not a place to ask for exhaustive tutorials; it's a place to come when you're stuck trying to get code working or if you don't understand something after having researched everything you can about it. This is why the comments on SO are (purposefully) restricted - they have to be short, no nice code formatting, etc. Comments on this site are intended for clarification, not as a discussion or forum thread.

Community
  • 1
  • 1
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks. I've actually read a few msdn blogs and your blog articles already. 'UI threads provide a context that will execute the next chunk on the UI thread' - it doesn't follow from what your intro post says about async/await, does it? I think a context is not a thread's identifier (many threads can share one context). Regarding _'What exactly is that “context”? Simple answer: If you’re on a UI thread, then it’s a UI context.'_ - it doesn't explain what _context_ really is. – user4205580 Nov 16 '15 at 09:15
  • So I guess 'UI threads provide a context that will execute the next chunk on the UI thread' follows from [this](https://msdn.microsoft.com/en-us/magazine/gg598924.aspx), quote: _The current implementation creates one WindowsFormsSynchronizationContext for each UI thread_. – user4205580 Nov 16 '15 at 09:49
  • @user4205580: It's there in the intro article, in the next paragraph after the "simple" explanation: `If SynchronizationContext.Current is not null, then it’s the current SynchronizationContext. (UI and ASP.NET request contexts are SynchronizationContext contexts).` – Stephen Cleary Nov 16 '15 at 13:03
  • Yes, but it doesn't say what that context behaves like, just as you explain in the [article](https://msdn.microsoft.com/en-us/magazine/gg598924.aspx). We don't know if the context will execute the contunation of async method on UI thread or not until we read about what SynchronizationContext is used. Don't get me wrong, I'm not complaining, I just want to know if I should have figured it out myself or not. – user4205580 Nov 16 '15 at 13:09
  • All I mean is that this sentence: _UI threads provide a context that will execute the next chunk on the UI thread_ doesn't follow from your blog post. We know that the next chunk will execute on UI context, but if executing on UI context (current synchronization context) means executing on the UI thread, we don't know that. We need details about what UI thread SynchronizationContext does: _The current implementation creates one WindowsFormsSynchronizationContext for each UI thread._, and it tells us that. – user4205580 Nov 16 '15 at 14:32
9

It is pretty simple, a thread can do only one thing at a time. So if you send your UI thread out in the woods doing something non-UI related, say a dbase query, then all UI activity stops. No more screen updates, no response to mouse clicks and key presses. It looks and acts frozen.

You'll probably say, "well, I'll just use another thread to do the UI then". Works in a console mode, kind of. But not in a GUI app, making code thread-safe is difficult and UI is not thread-safe at all because so much code is involved. Not the kind you wrote, the kind you use with a fancy class library wrapper.

The universal solution is to invert that, do the non-UI related stuff on a worker thread and leave the UI thread to only take care of the easy UI stuff. Async/await helps you do that, what's on the right of await runs on a worker. The only way to mess that up, and it is not uncommon, is to ask the UI thread to still do too much work. Like adding a line of text to a textbox once every millisecond. That's just bad UI design, humans don't read that fast.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 'what's on the right of await runs on a worker' - I think I've read a lot of answers (here on SO as well) stating that `await` itself doesn't create or switch threads. Calling a synchronous method by `await syncFoo()` will not make it execute on a different thread. Actually, there will be no difference between `syncFoo()` and `await syncFoo()`. – user4205580 Nov 16 '15 at 00:15
  • Sure, it doesn't. But nobody in his right mind will ever put anything on the right side that isn't asynchronous. And await will most definitely help anybody to get the right mind, not everything goes there. – Hans Passant Nov 16 '15 at 00:18
  • Sorry, maybe I'm wrong, but I think that even if the method is asynchronous, the async itself won't make it execute on a worker thread - it depends on whether that method calls `Task.Run` for example. Purely async methods usually don't use other threads, they perform I/O operations on a [lower level than threads](http://blog.stephencleary.com/2013/11/there-is-no-thread.html). – user4205580 Nov 16 '15 at 00:24
  • It isn't important at all that await code runs on a worker thread. Making code run on a worker is very, very easy. What *really* matters is what happens next. Getting code to run after the worker is done and getting it to run on the right thread. And dealing with the worker code failing. And knowing how to make it stop when you want to. Much, much harder. – Hans Passant Nov 16 '15 at 00:30
  • 'if the method is asynchronous, the **`await`** itself won't make it execute on a worker thread' - I meant await there. So the statement 'what's on the right of await runs on a worker' isn't entirely correct, because it all depends on what that thing on the right of await does inside. `await foo()` doesn't make `foo` run on a separate thread, no matter if `foo` is synchronous or not. – user4205580 Nov 16 '15 at 00:48
3

Given

async void Foo() {
   Bar();
   await Task.Yield();
   Baz();
}

you're right that if Foo() gets called on the UI thread, then Bar() gets called immediately, and Baz() gets called at some later time, but still on the UI thread.

However, this is not a property of the threads themselves.

What's actually going on is that this method gets split up into something similar to

Task Foo() {
   Bar();
   return Task.Yield().Continue(() => {
     Baz();
   });
}

This is not actually correct, but the ways in which it's wrong don't matter.

The argument that gets passed to my hypothetical Continue method is code that can be invoked in some way to be determined by the task. The task may decide to execute it immediately, it may decide to execute it at some later point on the same thread, or it may decide to execute it at some later point on a different thread.

Actually, the tasks themselves don't decide, they simply pass on the delegate to a SynchronizationContext. It's this synchronisation context that determines what to do with to-be-executed code.

And that's what's different between the thread types: once you access any WinForms control from a thread, then WinForms installs a synchronisation context for that specific thread, which will schedule the to-be-executed code at some later point on the same thread.

ASP.NET, background threads, it's all different synchronisation contexts, and that's what's causing the changes in how code gets scheduled.