【问题标题】:RunAsync - How do I await the completion of work on the UI thread?RunAsync - 我如何等待 UI 线程上的工作完成?
【发布时间】:2013-10-02 09:18:01
【问题描述】:

在等待Dispatcher.RunAsync 时,会在安排工作时发生延续,而不是在工作完成时。我怎样才能等待工作完成?

编辑

我最初的问题假设过早延续是由 API 的设计引起的,所以这是真正的问题。

当使用异步委托等待 Dispatcher.RunAsync 时,在委托代码中使用 await 时,会在遇到 await 时发生延续,而不是在工作完成时。我怎样才能等待工作完成?

编辑 2

您可能需要调度已经在 UI 线程上的工作的一个原因是要解决微妙的计时和布局问题。视觉树中元素的大小和位置值经常发生变化是很常见的,为 UI 的后续迭代安排工作会有所帮助。

【问题讨论】:

  • 这并不完全正确。当 lambda 返回时继续发生。当那个 lambda 实际上是 async void 时,那是在它实际完成之前。但是等待 RunAsync() 对于同步 lambdas 来说效果很好。
  • @svick 我也认为可能是这种情况,但我找不到任何关于 RunAsync 应该何时返回的文档。

标签: c# windows-runtime windows-store-apps async-await


【解决方案1】:

我在Microsoft github repository 上找到了以下建议:如何await a UI task sent from a background thread

设置

CoreDispatcher定义这个扩展方法:

using System;
using System.Threading.Tasks;
using Windows.UI.Core;

public static class DispatcherTaskExtensions
{
    public static async Task<T> RunTaskAsync<T>(this CoreDispatcher dispatcher, 
        Func<Task<T>> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal)
    {
        var taskCompletionSource = new TaskCompletionSource<T>();
        await dispatcher.RunAsync(priority, async () =>
        {
            try
            {
                taskCompletionSource.SetResult(await func());
            }
            catch (Exception ex)
            {
                taskCompletionSource.SetException(ex);
            }
        });
        return await taskCompletionSource.Task;
    }

    // There is no TaskCompletionSource<void> so we use a bool that we throw away.
    public static async Task RunTaskAsync(this CoreDispatcher dispatcher,
        Func<Task> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal) => 
        await RunTaskAsync(dispatcher, async () => { await func(); return false; }, priority);
}

完成此操作后,您需要做的就是使用新的RunTaskAsync 方法让您的后台任务等待 UI 工作。

使用示例

让我们假设这是需要在 UI 线程中运行的方法。注意调试语句,这将有助于遵循流程:

public static async Task<string> ShowMessageAsync()
{
    // Set up a MessageDialog
    var popup = new Windows.UI.Popups.MessageDialog("Question", "Please pick a button to continue");
    popup.Commands.Add(new Windows.UI.Popups.UICommand("Button 1"));
    popup.Commands.Add(new Windows.UI.Popups.UICommand("Button 2"));
    popup.CancelCommandIndex = 0;

    // About to show the dialog
    Debug.WriteLine("Waiting for user choice...");
    var command = await popup.ShowAsync();

    // Dialog has been dismissed by the user
    Debug.WriteLine("User has made a choice. Returning result.");
    return command.Label;
}

要从您的后台线程等待它,您将使用RunTaskAsync

// Background thread calls this method
public async void Object_Callback()
{
    Debug.WriteLine("Object_Callback() has been called.");

    // Do the UI work, and await for it to complete before continuing execution
    var buttonLabel = await Dispatcher.RunTaskAsync(ShowMessageAsync);
    
    Debug.WriteLine($"Object_Callback() is running again. User clicked {buttonLabel}.");
}

然后输出如下所示:

Object_Callback() 已被调用。

等待用户选择...

用户已做出选择。返回结果。

Object_Callback() 再次运行。用户点击了按钮 1。

【讨论】:

  • 嘿迈克,我将此作为官方答案,因为它是我的改进/通用版本。
  • 感谢您的回答!
  • 如何向“ShowMessageAsync”传递参数?如:ShowMessageAsync(string data)
  • 感谢您的解决方案!
  • 对我来说它不起作用。即使内部代码块完成,也不会解锁等待。所以await Dispatcher.RunTaskAsync后面的代码根本没有被调用。
【解决方案2】:

您的问题是假设您想安排(并等待)工作 UI 线程 后台线程。

如果您将 UI 设为“主”,而后台线程设为“奴隶”。

因此,不是让后台线程await 为 UI 线程执行某些操作(使用笨拙且不可移植的Dispatcher.RunAsync),而是让 UI 线程await 为后台线程执行某些操作做(使用可移植的、为异步制作的Task.Run)。

【讨论】:

  • 重点是等待分派的工作,无论该工作是从后台线程还是 UI 线程调用的。通常,后台(或 UI)线程需要确保在 UI 线程上完成某些操作,然后在完成后执行其他操作。我的解决方案的重点是在异步等待模式中进行编排,而不是诉诸事件或等待句柄。
  • @LukePuplett:我的意思是,这表明设计更加脆弱。有一次我“经常”不得不这样做,但是一旦我颠倒了这个逻辑,我的设计就变得更加清晰了。允许 UI 线程驱动逻辑比您的解决方案自然包含async/await更多,并且根本不使用事件或等待句柄。
  • 我认为您正在尝试用有关橙子的答案来回答有关苹果的问题。这里有一个非常具体的问题:知道这个 UI 工作何时完成。此 UI 工作位于自定义 UI 控件中,因此它必须协调其动作。说实话,这一切都发生在控件内的 UI 线程上。它只是一种使用 async-await 来编排一系列需要完成的工作的方式。您关于从 UI 线程驱动后台任务的观点总体上是正确的,以及我的工作方式,但它在这里不适用。它不会解决这个问题。
  • @LukePuplett 有时,一个非常具体的问题的解决方案是做一些完全不同的事情,也称为the XY problem
  • @LukePuplett 不,它没有。见await anything;的开头。
【解决方案3】:

您可以将对RunAsync 的调用包装在您自己的异步方法中,该方法可以等待并控制任务的完成,从而自己继续等待调用者。

由于 async-await 以 Task 类型为中心,因此您必须使用该类型来编排工作。但是,通常Task 将自己安排在线程池线程上运行,因此不能用于安排 UI 工作。

然而,TaskCompletionSource 类型的发明是为了充当不定期Task 的傀儡。换句话说,TaskCompletionSource 可以创建一个虚拟的Task,它没有计划做任何事情,但通过TaskCompletionSource 上的方法可以看起来像正常工作一样运行和完成。

看这个例子。

public Task PlayDemoAsync()
{
    var completionSource = new TaskCompletionSource<bool>();
    this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
    {
        try
        {
            foreach (var ppc in this.Plots.Select(p => this.TransformPlot(p, this.RenderSize)))
            {
                // For each subsequent stroke plot, we need to start a new figure.
                //
                if (this.Sketch.DrawingPoints.Any())
                    this.Sketch.StartNewFigure(ppc.First().Position);

                foreach (var point in ppc)
                {
                    await Task.Delay(100);

                    this.Sketch.DrawingPoints.Add(point.Position);
                }
            }

            completionSource.SetResult(true);
        }
        catch (Exception e)
        {
            completionSource.SetException(e);
        }
    });

    return (Task)completionSource.Task;
}

注意:UI 线程上的主要工作只是每 100 毫秒在屏幕上绘制一些线条。

TaskCompletionSource 被创建为 puppet master。查看接近尾声,您会看到它有一个Task 属性返回给调用者。返回Task 可以满足编译器的需求,并使方法可等待和异步。

但是,Task 只是一个傀儡,是 UI 线程中实际工作的代理。

看看我如何在主 UI 委托中使用 TaskCompletionSource.SetResult 方法将结果强制到 Task(因为返回给调用者)并传达工作已完成。

如果出现错误,我会使用 SetException 来“拉另一个字符串”,并让它看起来像是在 puppet Task 中冒出异常。

async-await 子系统没有什么不同,因此它可以按您的预期工作。

编辑

根据 svick 的提示,如果该方法设计为只能从 UI 线程调用,那么这就足够了:

    /// <summary>
    /// Begins a demonstration drawing of the asterism.
    /// </summary>
    public async Task PlayDemoAsync()
    {
        if (this.Sketch != null)
        {
            foreach (var ppc in this.Plots.Select(p => this.TransformPlot(p, this.RenderSize)))
            {
                // For each subsequent stroke plot, we need to start a new figure.
                //
                if (this.Sketch.DrawingPoints.Any())
                    this.Sketch.StartNewFigure(ppc.First().Position);

                foreach (var point in ppc)
                {
                    await Task.Delay(100);

                    this.Sketch.DrawingPoints.Add(point.Position);
                }
            }
        }
    }

【讨论】:

  • 你问这个问题是为了自己回答?
  • 是的。这就是质量检查的目的。
  • 如果你经常这样做,创建一个辅助方法是有意义的,它将在try中执行的代码作为async Task lambda。
  • 另外,您不必必须为此使用Task,但这可能是最合理的选择。
【解决方案4】:

即使您出于某种原因必须从工作线程开始,@StephenCleary 建议的干净方式工作的好方法是使用简单的辅助对象。使用下面的对象,您可以编写如下代码:

    await DispatchToUIThread.Awaiter;
    // Now you're running on the UI thread, so this code is safe:
    this.textBox.Text = text;

在您的 App.OnLaunched 中,您必须初始化对象:

    DispatchToUIThread.Initialize(rootFrame.Dispatcher);

您可以在await anything;找到以下代码背后的理论

public class DispatchToUIThread : INotifyCompletion
{
    private readonly CoreDispatcher dispatcher;

    public static DispatchToUIThread Awaiter { get; private set; }

    private DispatchToUIThread(CoreDispatcher dispatcher)
    {
        this.dispatcher = dispatcher;
    }

    [CLSCompliant(false)]
    public static void Initialize(CoreDispatcher dispatcher)
    {
        if (dispatcher == null) throw new ArgumentNullException("dispatcher");
        Awaiter = new DispatchToUIThread(dispatcher);
    }

    public DispatchToUIThread GetAwaiter()
    {
        return this;
    }

    public bool IsCompleted
    {
        get { return this.dispatcher.HasThreadAccess; }
    }

    public async void OnCompleted(Action continuation)
    {
        if (continuation == null) throw new ArgumentNullException("continuation");
        await this.dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => continuation());
    }

    public void GetResult() { }
}

【讨论】:

  • async-await的原始预览中,有一种方法可以做到这一点。 But it was removed, because doing it this way is considered a bad practice.
  • @svick 这可能被认为是不好的做法,但有时(比如当你想在 SplashScreen.Dismissed 事件中做与 UI 相关的事情时)你别无选择。在我提到的示例中,我发现 SplashScreen.Dismissed 事件不在 UI 线程上运行,这意味着您希望在关联的处理程序中执行的操作是禁止的——除非您使用此类强制它运行UI线程上的其余部分。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-16
  • 1970-01-01
  • 2017-05-15
  • 2021-10-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多