27
C# Async Await, Eventually: UI Threads
So I am getting to the end of this series which is to finally describe async and await in C# more deeply but I am not there yet!
So far I have explained asynchronous programming and we have just dipped a toe into thread pools.
To more deeply and debug performance problems associated with using async/await, you need to understand the underlying threading models. I have already looked at thread pools and now I will discuss UI threads.
UI threads are threads that perform UI rendering and user interaction related activities. Blocking UI threads is problematic because it reduces the responsiveness of your UI application.
To identify the UI threads we need to delve into different UI technologies.
Windows Presentation Foundation (WPF) was initially released in 2006 and is still in active use today. As of today, it's latest stable release was in April 2021. With respect to .NET 5, WPF is not cross platform and is only supported for Windows. There is, however, a port path from .NET Framework to .NET 5 for Windows that makes use of the compatibility pack.
All WPF applications have at least 2 UI threads: one thread for rendering, and another (typically one) thread which handles user interaction and runs your UI code. The rendering thread is hidden and runs in the background. Most UI applications only run one additional UI thread and work is co-ordinated using a Dispatcher. By keeping the work items executed by the Dispatcher small, your UI will remain responsive.
Other non-UI threads can be spawned to run non-UI related work and a thread pool can be used to manage these threads.
Blazor is a relatively new cross-platform web UI technology that was first released in 2018 with .NET Core 3. It is included with .NET 5.
Blazor can run in one of 2 modes: where your UI code is executed client-side or where your UI code is executed server-side.
When running on the client-side, your code runs in the browser and WebAssembly is used - no JavaScript! (To oversimplify WebAssembly: It is an instruction set that runs in a sandboxed virtual-machine-like environment in the browser).
Running your code using Blazor client-side, there are no background threads - your UI code runs in a single thread. This means you will need to be extra careful to break up large jobs CPU intensive into smaller pieces and yielding between pieces to ensure the UI remains responsive. More generally, you must not use blocking calls otherwise your UI will freeze.
When running on the server-side, your code runs on the server. UI events are shuttled to the server using SignalR (an asynchronous library that can use various network transports for bi-directional communication between client and server). UI updates are propagated back to the DOM managed by the client-side browser.
In this case the server-side runs as a ASP.NET core server application. Kestrel is the default cross-platform web server. A thread pool is used to manage processing of incoming and outgoing communications and other units of work.
When running with a thread pool threading model, you might find you can get away with some blocking operations but this will leave your application vulnerable to thread pool starvation.
Thread pool starvation at best looks like poor performance especially under load. At worst thread pool starvation can lead to your application locking up altogether! It is a condition that is getting easier to diagnose with the latest .NET tools but it still has a nasty habit of popping up when you think you least expect it.
(If you find you need to do this, you may have a case of thread-pool-starvitis!)
It doesn't really matter which threading model is used, the general mantra with respect to async/await is: Don't Use Blocking Operations. I will delve into examples of this in a future post but for now I think we are ready to look at async/await and hopefully understand it more deeply in my next post.
27