【问题标题】:How to await an async UI method from a different thread?如何等待来自不同线程的异步 UI 方法?
【发布时间】:2015-06-30 01:43:58
【问题描述】:

我如何优雅地告诉我的应用程序它应该等待某个异步 (Ask()) 方法的结果,而不是在其当前 (Game) 线程上而是在不同的 ( UI) 线程?

我有一个带有两个线程的表单应用程序

  • 强制UI Thread,运行用户界面
  • 还有第二个 Game Thread,它以一种无限循环的形式运行,等待输入操作并以或多或少恒定的帧速率渲染游戏视图。

用户界面由两种形式组成:

  • 一个简单的MainForm,带有一个Create Cube 按钮、一个Create Sphere 按钮和一个渲染视图
  • 还有一个自定义的ChoiceForm,它要求用户使用两个相应的按钮在Sharp CornersRounded Corners 之间进行选择。

当用户单击Create Cube 按钮时,UI Thread 将处理此单击事件并(同步)将新的()=>Game.Create<Cube>() 操作排队以由Game Thread 处理。

Game Thread 将在处理另一个帧时获取此操作,并检查用户是否要创建 CubeSphere。如果用户请求Cube,它应该使用第二种形式询问用户所需的立体角形状。

问题是,在等待用户决定时,UIGame 线程都不应该被阻塞。因此,Task Game.Create<T>(...) 方法和Task<CornerChoice> ChoiceForm.Ask()methods 被声明为异步。 Game Thread 将等待Create<T>() 方法的结果,而该方法又应该在UI 线程上等待Ask() 方法的结果(因为ChoiceForm 是在该方法内部创建并显示的)。

如果这一切都发生在一个 UI Thread 上,生活会相对容易,Create 方法看起来像这样:

public class Game
{
    private async Task Create<IShape>()
    {
        CornerChoice choice = await ChoiceForm.Ask();
        ...
    }
}

经过反复试验,我想出了以下(实际可行的)解决方案,但每次我仔细观察它时,它似乎都会伤害到我内心的某个地方(尤其是 Create 方法中的 Task&lt;Task&lt;CornerChoice&gt;&gt; 部分):

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

【问题讨论】:

  • 你的问题是什么?
  • 有没有比我使用 await await Task> 在不同线程上等待任务更好的方法?
  • 为什么游戏需要了解有关 UI 的任何信息?如果你想让它创建一个Shape,你需要在用户告诉你他想要一个立方体还是一个球体之后告诉他(你的Game对象)。
  • @YuvalItzchakov 实际应用程序不是游戏,而是具有常规用户界面和一个或多个由第二个线程运行的渲染视图的应用程序。因为并不总是立即清楚,需要哪些进一步的步骤来完成这样的“创建”请求,所以第二个(视口)线程需要有可能向用户呈现额外的问题并等待他们的结果,而不会中断渲染过程。
  • 你可以尝试用Task.Run替换Task.Factory.StartNew吗,在这种情况下你不需要创建像Task>这样的类型,我将粘贴一个我有的简单示例在下面创建。它可能无法回答您的完整问题,但试图解决问题的子部分

标签: c# multithreading asynchronous async-await task


【解决方案1】:

在阅读了可能相关的问题here 和 Stephen Dougs 的博客文章 Task.Run vs Task.Factory.StartNew 由 Stephen Cleary 链接并与 Mrinal Kamboj 讨论了这个问题后,我得出的结论是 Task.Run 方法有点像 @ 的包装987654324@ 用于常见情况。因此,对于我不常见的情况,我决定将导致痛苦的东西扫入扩展方法中,以使调用如下所示:

private async Task Create<IShape>()
{
    ...
    CornerChoice choice = await _uiScheduler.Run(ChoiceForm.Ask);
    ...
}

使用相应的扩展方法:

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

似乎也没有必要将() =&gt; ChoiceForm.Ask() lambda 声明为async

【讨论】:

  • 这是一个有效的实现,但耦合度较低的解决方案是让 UI 公开一个像 IAskChoice 这样的“服务”,而不是让后台线程知道 ChoiceForm 或任务调度程序(或任何用户界面)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多