How can I elegantly tell my application that it should await the result of some async (Ask()
) method not on its current (Game
) thread but on a different (UI
) thread instead?
I've got a Forms application with two threads
- the obligatory
UI Thread
, which runs the user interface - and a second
Game Thread
, that runs in a sort of infinite loop, waiting for input actions and rendering the game view at a more or less constant framerate.
The user interface consists of two forms:
- a simple
MainForm
with aCreate Cube
button, aCreate Sphere
button and a rendering view - and a custom
ChoiceForm
that asks the user to choose betweenSharp Corners
andRounded Corners
using two according buttons.
When the user clicks on the Create Cube
button, the UI Thread
will handle this click event and (synchronously) queue a new ()=>Game.Create<Cube>()
action to be processed by the Game Thread
.
The Game Thread
will fetch this action when it is processing another frame and check if the user wanted to create a Cube
or a Sphere
. And if the user requested a Cube
it should ask the user using the second form about the desired shape for the cube corners.
The problem is, that neither the UI
nor the Game
thread should be blocked while waiting for the user decision. Because of this the Task Game.Create<T>(...)
method and the Task<CornerChoice> ChoiceForm.Ask()
methods are declared as async. The Game Thread
will await the result of the Create<T>()
method, which in its turn should await the result of the Ask()
method on the UI thread (because the ChoiceForm
is created and displayed right inside of that method).
If this all would happen on a single UI Thread
life would be relatively easy and the Create
method would look like this:
public class Game
{
private async Task Create<IShape>()
{
CornerChoice choice = await ChoiceForm.Ask();
...
}
}
After some trial and error I came up with the following (actually working) solution, but it seem to hurt me somewhere inside each time I look at it closely (especially the Task<Task<CornerChoice>>
part in the Create
method):
public enum CornerChoice {...}
public class ChoiceForm
{
public static Task<CornerChoice> Ask()
{
...
}
}
public class MainForm
{
private readonly Game _game;
public MainForm()
{
_game = new Game(TaskScheduler.FromCurrentSynchronizationContext());
}
...
}
public class Game
{
private readonly TaskScheduler _uiScheduler;
public Game(TaskScheduler uiScheduler)
{
_uiScheduler = uiScheduler;
}
private async Task Create<IShape>()
{
...
Task<CornerChoice> task = await Task<Task<CornerChoice>>.Factory.StartNew(
async () => await ChoiceForm.Ask(),
CancellationToken.None, TaskCreationOptions.None, _uiScheduler);
CornerChoice choice = await task;
...
}
}