12

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

编辑

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

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

编辑 2

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

4

4 回答 4

16

我在Microsoft github 存储库中找到了以下建议:如何等待从后台线程发送的 UI 任务

设置

为 定义此扩展方法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。

于 2016-07-01T00:08:09.187 回答
9

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

如果您将UI设为“主”而后台线程设为“从属”,您通常会发现您的代码更清晰、更易于理解(而且肯定会更便携) 。

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

于 2013-10-02T13:38:27.067 回答
5

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

由于 async-await 以Task类型为中心,因此您必须使用此类型来编排工作。但是,通常 aTask将自己安排在线程池线程上运行,因此不能用于安排 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 毫秒在屏幕上绘制一些线条。

ATaskCompletionSource被创建为人偶大师。查看接近尾声,您会看到它有一个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);
                }
            }
        }
    }
于 2013-10-02T09:18:01.313 回答
1

即使出于某种原因必须从工作线程开始,@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 任何东西中找到下面代码背后的理论;

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() { }
}
于 2014-03-14T09:23:53.740 回答