How to wait for an asynchronous UI method from another thread?
How can I gracefully tell my application that it should expect the result of some async ( Ask()
) method not on its current thread ( Game
), but on another ( UI
) thread?
I have a Forms application with two threads
- mandatory
UI Thread
, which launches the user interface - and the second
Game Thread
, which runs as an endless loop, waiting for input and rendering of the game view at a more or less constant frame rate.
The user interface consists of two forms:
- simple
MainForm
with buttonCreate Cube
, buttonCreate Sphere
and render view - and a custom
ChoiceForm
one that prompts the user to choose betweenSharp Corners
andRounded Corners
using two corresponding buttons.
When the user clicks the button Create Cube
, it UI Thread
handles that click event and (synchronously) the next new action ()=>Game.Create<Cube>()
to be handled Game Thread
.
Game Thread
will take this action while processing another frame and check if the user wants to create Cube
or Sphere
. And if the user requested Cube
, it should ask the user to use a second shape about the desired shape for the corners of the cube.
The problem is that neither thread UI
nor thread Game
should block while waiting for the user to decide. Because of this, the method Task Game.Create<T>(...)
and methods Task<CornerChoice> ChoiceForm.Ask()
are declared as async. Game Thread
waits for the result of the method Create<T>()
, which in turn should wait for the result of the method Ask()
on the UI thread (since it ChoiceForm
is created and displayed right inside that method).
If it all happened in one UI Thread
, life would be relatively easy, and the method Create
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 seems to hurt me somewhere inside every time I look at it very closely (especially the part Task<Task<CornerChoice>>
in the method Create
):
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;
...
}
}
source to share
After reading possibly a related question here and Stephen Doug's blog post Task.Run vs Task.Factory.StartNew , linked by Stephen Cleary and discussing this with Mrinal Cambodia, I concluded that the method Task.Run
is sort of a wrapper around TaskFactory.StartNew
for regular cases. So for my less common case, I decided to sweep the painful substances into an extension method to make the appeal like this:
private async Task Create<IShape>()
{
...
CornerChoice choice = await _uiScheduler.Run(ChoiceForm.Ask);
...
}
With the appropriate 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 there is also no need to declare the () => ChoiceForm.Ask()
lambda as async
.
source to share
Below is a program I created on LinqPad, it does a similar job, when we use Task.Factory.StartNew
, then we need to use the type like Task<Task<T>>
, but this is not the case forTask.Run
void Main()
{
for(int counter=0; counter < 3; counter++)
{
// Start computation (Asynchronously) and control will move forward for UI thread without blocking
TestAsync();
// Control will immediately return after executing the Async method
// Handle user input
string input = Console.ReadLine();
Console.WriteLine("User Input :: " + input);
}
}
static async void TestAsync()
{
// Using Task.Run
// int x = await Task.Run(()=>Allocate());
// Console.WriteLine("Done, Result -- " + x);
// Using Task.Factory.StartNew
Task<int> x = await Task<Task<int>>.Factory.StartNew(()=>Allocate());
Console.WriteLine("Done, Result -- " + x.Result);
}
static async Task<int> Allocate()
{
await Task.Delay(3000);
return 100;
}
source to share