user4205580
user4205580

Reputation: 572

What's so special about UI thread?

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.

Upvotes: 6

Views: 3109

Answers (3)

Stephen Cleary
Stephen Cleary

Reputation: 457197

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.

Upvotes: 10

Hans Passant
Hans Passant

Reputation: 942255

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.

Upvotes: 9

user743382
user743382

Reputation:

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.

Upvotes: 3

Related Questions