web-dev-qa-db-ja.com

RunAsync-UIスレッドでの作業の完了を待つにはどうすればよいですか?

待っているときDispatcher.RunAsync継続は、作業が完了したときではなく、作業がスケジュールされたときに発生します。作業が完了するのをどのように待つことができますか?

編集

私の最初の質問は、時期尚早の継続がAPIの設計によって引き起こされたと想定していたので、ここに本当の質問があります。

待っているときDispatcher.RunAsync非同期デリゲートを使用し、デリゲートのコード内でawaitを使用すると、作業が完了したときではなく、awaitが検出されたときに継続が発生します。作業が完了するのをどのように待つことができますか?

編集2

すでにUIスレッド上にある作業をディスパッチする必要がある理由の1つは、微妙なタイミングとレイアウトの問題を回避するためです。ビジュアルツリー内の要素のサイズと位置の値が流動的であるのは非常に一般的であり、UIのその後の反復のために作業をスケジュールすることが役立つ場合があります。

12
Luke Puplett

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をクリックしました。

15
Mike

あなたの質問は、あなたが仕事をスケジュールする(そして待つ)ことを想定していますon UIスレッドfromバックグラウンドスレッド。

[〜#〜] ui [〜#〜]が「マスター」である場合、通常、コードははるかにクリーンで理解しやすい(そして間違いなく移植性が高い)ことがわかります。バックグラウンドスレッドは「スレーブ」です。

したがって、バックグラウンドスレッドawaitを使用する代わりに、UIスレッドが実行する操作(厄介で移植性のないDispatcher.RunAsync)、UIスレッドawaitバックグラウンドスレッドが実行する操作があります(ポータブルで非同期用に作成されたTask.Run)。

8
Stephen Cleary

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がパペットマスターとして作成されます。終わり近くを見ると、呼び出し元に返されるTaskプロパティがあることがわかります。 Taskを返すと、コンパイラのニーズが満たされ、メソッドが待機可能で非同期になります。

ただし、Taskは単なる操り人形であり、UIスレッドで行われている実際の作業のプロキシです。

そのメインUIデリゲートで、TaskCompletionSource.SetResultメソッドを使用して結果をTaskに強制し(呼び出し元に返されたため)、作業が終了したことを通知する方法を確認してください。

エラーが発生した場合は、SetExceptionを使用して「別の文字列をプル」し、パペット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);
                }
            }
        }
    }
5
Luke Puplett

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

以下のコードの背後にある理論は 何でも待つ; で見つけることができます

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() { }
}
1
Marcel W