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 button Create Cube

    , button Create Sphere

    and render view
  • and a custom ChoiceForm

    one that prompts the user to choose between Sharp Corners

    and Rounded 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;
        ...
    }
}

      

+3


source to share


2 answers


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

.

0


source


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;     
    }

      

0


source







All Articles