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
UI Thread
, which runs the user interface 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:
MainForm
with a Create Cube
button, a Create Sphere
button and a rendering view ChoiceForm
that asks the user to choose between Sharp Corners
and Rounded 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;
...
}
}
After reading a probably related question here and Stephen Dougs blog post Task.Run vs Task.Factory.StartNew linked by Stephen Cleary and discussing this question with Mrinal Kamboj I was led to the conclusion that the Task.Run
method is sort of a wrapper around TaskFactory.StartNew
for the common cases. So for my not-so-common case I decided to sweep the pain-causing stuff into an extension method to make the invocation look like following:
private async Task Create<IShape>()
{
...
CornerChoice choice = await _uiScheduler.Run(ChoiceForm.Ask);
...
}
With the according extension method:
public static class ExtensionsForTaskScheduler
{
public static async Task<T> Run<T>(this TaskScheduler scheduler,
Func<Task<T>> scheduledTask)
{
return await await Task<Task<T>>.Factory.StartNew(scheduledTask,
CancellationToken.None, TaskCreationOptions.None, scheduler);
}
}
It seems that it was also not necessary to declare the () => ChoiceForm.Ask()
lambda as async
.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.