Introduction
The general problem a user is faced with when developing a Winforms or a WPF application, is that the UI of those frameworks is single threaded.
Running asynchronous code, while possible via secondary threads, leaves the user unable to make UI calls on the main thread, without using Invoke workarounds.
The problem is aggravated if the user needs to execute delayed code, since Thread.Sleep
hangs the entire thread, making the UI unresponsive.
The use of async
/await
to solve the thread issue doesn't come without its own list of problems. Using await
/async
on any asynchronous methods creates async
dependencies propagating to all methods calling the current one, and all methods the current one calls.
Pretty fast, the entire code is infested with async
methods, even though the user only wanted one of them to be asynchronous.
The game engine Unity, took the idea of creating Coroutines through the use of IEnumerable
collections, and fleshed it out, and based the entire engine around it.
Through slicing, we can achieve a virtually asynchronous parallel execution on the main thread. This prevents the creation of additional threads, and keeps our code clean from redundant async
/await
calls while not interfering with the UI thread.
Background
For anyone unfamiliar with the use of IEnumerator
Coroutines, Unity has a brief overview on their website, as seen here.
Using the Code
The main challenge in implementing Unity's Coroutine framework, is that it requires a game loop. Slicing is frame based, and requires constantly executed iterative code in order to traverse all registered coroutines.
Winforms and WPF however do not have an exposed Main loop. They are both event driven frameworks based on registered delegates.
On Winforms, generating something like a game loop proved especially tricky. Eventually, we decided to follow Tom Miller's approach, described in his blog post here.
His blog describes how we can approximate a continuous loop on the main thread by waiting for all thread operations to end, and then looping continuously until a new operation requires to use the thread.
This approach, while working adequately, does create a spin, and therefore can be CPU intensive. The only thing we had to change from that implementation, was the added Refresh()
on the main form. This was necessary since the thread goes completely inactive if no controls are being modified on it.
For WPF, the main loop was easier to simulate. WPF gives us an OnRender
event through CompositionTarget.Rendering
. All that was needed to be done was to subscribe the main loop callback to that event.
This means that since no spins are generated, the WPF implementation of the Coroutine Framework is much less CPU intensive than its Winforms counterpart.
Implementing the framework is very simple. All Coroutine related classes are in the UnityCoroutines namespace
. After importing it, all that needs to be done is to call the CoroutineManager
singleton's Run
method once, preferably after UI initialization.
CoroutineManager.Instance.Run(this);
CoroutineManager.Instance.Run();
This call sets up the coroutine loop, and also a Time
loop that keeps track of deltaTime
, unscaledDeltaTime
, time
and timeScale
variables. These variables are required in order to perform time-relevant asynchronous execution.
Just like in Unity, we initialize and run a coroutine through StartCoroutine
, by providing it as an argument in the form of an IEnumerator
. This coroutine is then sliced and executed sequentially in parallel to the UI code.
Unlike Unity however, StartCoroutine
, as with all Coroutine
related methods, exists in the CoroutineManager
singleton and must be executed through it.
The following code (which also exists in Form1.cs and MainWindow.xaml.cs, shows an example of a timer running in parallel to the UI thread, increasing its counter by 1 every 5 seconds.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CoroutineManager.Instance.Run();
CoroutineManager.Instance.StartCoroutine(CountDown());
}
private IEnumerator CountDown()
{
int counter = 0;
while(counter < 10)
{
counter++;
Counter.Content = "Counter : " + counter;
yield return CoroutineManager.Instance.StartCoroutine(WaitForSeconds(5f));
}
}
private IEnumerator WaitForSeconds(float seconds)
{
float time = 0f;
while(time < seconds)
{
EllapsedSeconds.Content = "Elapsed Seconds : " + time;
DeltaTime.Content = "Delta Time : " + Time.deltaTime;
time += Time.deltaTime;
yield return null;
}
}
}
Notice that just as in Unity, we can get the return of the StartCoroutine
method and yield it as well, in essence pausing execution until the called coroutine completes its own execution.
Points of Interest
The framework can be expanded. A YieldInstruction interface
has been created, and the CoroutineManager
shows where its use should be. Therefore, classes properly implementing the YieldInstruction interface
can be yielded by the CoroutineManager
.