1. ホーム
  2. c#

[解決済み] WPF GUIから非同期タスクを実行し、対話する方法

2023-05-22 06:26:13

質問

WPF GUI で、長いタスクを開始するためにボタンを押すと、そのタスクの間ウィンドウがフリーズしてしまうことがあります。タスクが実行されている間、進捗状況のレポートを取得したいのですが、私が選んだいつでもタスクを停止する別のボタンを組み込みたいと思います。

async/await/taskを使用する正しい方法を把握することができません。私が試したすべてを含めることはできませんが、これは私が現在持っているものです。

WPFのウィンドウクラスです。

public partial class MainWindow : Window
{
    readonly otherClass _burnBabyBurn = new OtherClass();
    internal bool StopWorking = false;
        
    //A button method to start the long running method
    private async void Button_Click_3(object sender, RoutedEventArgs e)
    {   
        Task slowBurn = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);
            
        await slowBurn;
    }
        
    //A button Method to interrupt and stop the long running method
    private void StopButton_Click(object sender, RoutedEventArgs e)
    {
        StopWorking = true;
    }

    //A method to allow the worker method to call back and update the gui
    internal void UpdateWindow(string message)
    {
        TextBox1.Text = message;
    }
}
 

そして、ワーカーメソッド用のクラスです。

class OtherClass
{
    internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
    {       
        var tcs = new TaskCompletionSource<int>();       
             
        //Start doing work
        gui.UpdateWindow("Work Started");        
             
        While(stillWorking)
        {
        //Mid procedure progress report
        gui.UpdateWindow("Bath water n% thrown out");        
        if (gui.StopTraining) return tcs.Task;
        }
             
        //Exit message
        gui.UpdateWindow("Done and Done");       
        return tcs.Task;        
    }
}

これは実行されますが、Workerメソッドが開始されると、WPF機能ウィンドウはまだブロックされたままです。

async/await/taskの宣言をどのようにアレンジすれば

A) ワーカーメソッドがGUIウィンドウをブロックしないようにする。

B) ワーカーメソッドがguiウィンドウを更新するようにする。

C) guiウィンドウが割り込みを停止し、ワーカーメソッドを停止するようにする。

どんなヘルプやポインタでも大いに結構です。

どのように解決するのですか?

長い話です。

private async void ButtonClickAsync(object sender, RoutedEventArgs e)
{
    // modify UI object in UI thread
    txt.Text = "started";

    // run a method in another thread
    await HeavyMethodAsync(txt);
    // <<method execution is finished here>>

    // modify UI object in UI thread
    txt.Text = "done";
}

// This is a thread-safe method. You can run it in any thread
internal async Task HeavyMethodAsync(TextBox textBox)
{
    while (stillWorking)
    {
        textBox.Dispatcher.Invoke(() =>
        {
            // UI operation goes inside of Invoke
            textBox.Text += ".";
            // Note that: 
            //    Dispatcher.Invoke() blocks the UI thread anyway
            //    but without it you can't modify UI objects from another thread
        });
        
        // CPU-bound or I/O-bound operation goes outside of Invoke
        // await won't block UI thread, unless it's run in a synchronous context
        await Task.Delay(51);
    }
}

Result:
started....................done


(1)の書き方について知っておく必要があります。 async のコードの書き方 (2) 別のスレッドでUI操作を実行する方法 (3) タスクをキャンセルする方法について知っておく必要があります。

この記事では(3)のキャンセルの仕組みについては触れません。ただ、この記事では CancellationTokenSource を生成し、それによって CancellationToken を与え、それを任意のメソッドに渡すことができます。ソースをキャンセルすると、すべてのトークンが知ることができます。


async そして await :

の基本 asyncawait

  1. 以下の場合のみ可能です。 await の中に async メソッドに追加します。

  2. のみが可能です。 await は、待ち受け可能なオブジェクト(つまり Task , ValueTask , Task<T> , IAsyncEnumerable<T> など) これらのオブジェクトは async メソッドと await キーワードはそれらをアンラップします。(ラッピングとアンラッピングのセクションを参照)

  3. 非同期メソッド名は 常に で終わらせる。 Async で終わるようにすると、読みやすく、間違えにくくなります。

    // Synchronous method:
    TResult MethodName(params) { }
    
    // Asynchronous method:
    async Task<TResult> MethodNameAsync(params) { }
    
    

の魔法 asyncawait

  1. async-await 構文機能は、ステートマシンを使って、コンパイラに をあきらめる 引き取る の制御は awaited Taskasync メソッドに追加します。

  2. で実行を待ちます。 await で待ち、その結果を返します。 ブロックすることなく をブロックすることなく、結果を返します。

  3. Task.Run をキューに入れます。 Task の中に スレッドプール . (ただし、それが 純粋な の操作でなければ)。 すなわち async メソッド は別のスレッドで実行されます。 asyncawait はそれ自体ではスレッド生成とは関係ありません。

だから

あなたが を実行すると Task (例 Task.Run(action) のように)その動作のためにスレッドを(再)使用します。そして、そのタスクを async メソッドに入れることで、その流れを制御することができます。を置くことで async をつけると、コンパイラにステートマシンを使ってそのメソッドのフローを制御するように指示します(これはスレッド化を意味するものではありません)。また await を使うことで、そのメソッド内の実行フローが await ステートメント UIスレッドをブロックすることなく . もし、フローを呼び出し側に渡したい場合は async メソッド自体が Task になるので、同じパターンを呼び出し元などにカスケードすることができるようになります。

async Task Caller() { await Method(); }
async Task Method() { await Inner(); }
async Task Inner() { await Task.Run(action); }

イベントハンドラは以下のようなコードになります。

のシグネチャにasyncが存在する場合、2つのケースが考えられます。 ExecuteLongProcedure (ケース 1 と 2) と MyButton_ClickAsync (ケースA,B)について説明します。

private async void MyButton_ClickAsync(object sender, RoutedEventArgs e)
{
    //queue a task to run on threadpool

    // 1. if ExecuteLongProcedure is a normal method and returns void
    Task task = Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3)
    );
    // or
    // 2. if ExecuteLongProcedure is an async method and returns Task
    Task task = ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);

    // either way ExecuteLongProcedure is running asynchronously here
    // the method will exit if you don't wait for the Task to finish

    // A. wait without blocking the main thread
    //  -> requires MyButton_ClickAsync to be async
    await task;
    // or
    // B. wait and block the thread (NOT RECOMMENDED AT ALL)
    // -> does not require MyButton_ClickAsync to be async
    task.Wait();
}


非同期メソッドの戻り値の型。

次のような宣言があったとします。

private async ReturnType MethodAsync() { ... }

  • もし ReturnTypeTask では await MethodAsync(); が返ってくる void

  • もし ReturnTypeTask<T> では await MethodAsync(); は型の値を返します。 T

    これは アンラッピング と呼ばれ、次のセクション(Wrapping and Unrwapping)を参照してください。

  • もし ReturnTypevoid できない await それ

    • もし、あなたが await MethodAsync(); と書くと、コンパイルエラーが発生します。

    cannot await void

    • 以下のことが可能です。 だけ 燃やして忘れる すなわち、普通にメソッドを呼び出すだけです。 MethodAsync(); を呼び出すだけで、あとは自分の人生を歩むだけです。
    • MethodAsync の実行は同期的に行われますが、これは async を持つので、そのマジックを利用することができます。 await task をメソッド内に書くことで、実行の流れを制御することができます。
    • これは、WPFがボタンのクリックイベントハンドラを処理する方法です。 明らかに、イベントハンドラが void .

非同期メソッドの戻り値の型は必ず void , Task , Task<T> のように、タスクのようなタイプです。 IAsyncEnumerable<T> または IAsyncEnumerator<T>


ラッピングとアンラッピング。

ラッピング。

async メソッドはその戻り値を Task .

例えば、このメソッドは Task の周りに int で囲み、それを返す。

//      async Task<int>
private async Task<int> GetOneAsync()
{
    int val = await CalculateStuffAsync();
    return val;
//  returns an integer
}

ラッピングを解除します。

取得する場合や アンラップ である値を ラップ の中にある Task<> :

await はラップを解き int から Task :

Task<int> task = GetOneAsync();
int number = await task;
//int     <-       Task<int>

ラップとアンラップの異なる方法。

private Task<int> GetNumber()
{
    Task<int> task;

    task = Task.FromResult(1); // the correct way to wrap a quasi-atomic operation, the method GetNumber is not async
    task = Task.Run(() => 1); // not the best way to wrap a number

    return task;
}

private async Task<int> GetNumberAsync()
{
    int number = await Task.Run(GetNumber); // unwrap int from Task<int>

    // bad practices:
    // int number = Task.Run(GetNumber).GetAwaiter().GetResult(); // sync over async
    // int number = Task.Run(GetNumber).Result; // sync over async
    // int number = Task.Run(GetNumber).Wait(); // sync over async

    return number; // wrap int in Task<int>
}

まだ迷っていますか?非同期の戻り値の型は MSDN .

タスクの結果をアンラップするには 常に を使うようにしてください。 await の代わりに .Result に変更しないと、非同期の利点はなく、非同期の欠点だけが残ることになります。後者は「sync over async"」と呼ばれます。

注意してください。

await は非同期で task.Wait() とは異なります。しかし、どちらもタスクが終了するのを待つという点では同じです。

await は非同期で task.Result とは異なります。しかし、どちらもタスクの終了を待ち、ラップを解除して結果を返すという点では同じです。

ラップされた値を持つには、常に Task.FromResult(1) を使って新しいスレッドを作成するのではなく Task.Run(() => 1) .

Task.Run は、より新しく(.NetFX4.5)、よりシンプルなバージョンです。 Task.Factory.StartNew


WPFのGUIです。

ここで説明するのは UI操作を別のスレッドで実行する方法について説明します。

ブロック化する。

まず最初に知っておくべきことは WPF非同期イベントハンドラ Dispatcher を提供することになります。 同期コンテキスト . ここで説明されている

CPUバウンドまたはIOバウンドの操作、例えば Sleeptask.Wait() ブロックし、消費する を持つメソッドで呼び出されたとしても、スレッドをブロックし、消費します。 async キーワードを持つメソッドで呼び出されたとしてもです。 await Task.Delay() はステートマシンに 停止 を停止させ、スレッドのリソースを消費させないようにします。

private async void Button_Click(object sender, RoutedEventArgs e)
{
        Thread.Sleep(1000);//stops, blocks and consumes threadpool resources
        await Task.Delay(1000);//stops without consuming threadpool resources
        Task.Run(() => Thread.Sleep(1000));//does not stop but consumes threadpool resources
        await Task.Run(() => Thread.Sleep(1000));//literally the WORST thing to do
}

スレッドの安全性

GUIに非同期でアクセスする必要がある場合( ExecuteLongProcedure メソッド内)。 を呼び出します。 スレッドセーフでないオブジェクトへの変更を伴う操作。例えば、WPFのGUIオブジェクトはすべて Dispatcher オブジェクトはGUIスレッドに関連付けられます。

void UpdateWindow(string text)
{
    //safe call
    Dispatcher.Invoke(() =>
    {
        txt.Text += text;
    });
}

ただし、タスクの起動が プロパティ変更コールバック の結果としてタスクが開始されるのであれば、ViewModel からは Dispatcher.Invoke を使用する必要はありません。なぜなら、コールバックは実際には UI スレッドから実行されるからです。

非UIスレッドでコレクションにアクセスする

<ブロッククオート

WPF では、コレクションを作成したスレッド以外のスレッドでデータ コレクションにアクセスし、変更することができます。これにより、バックグラウンド スレッドを使用して、データベースなどの外部ソースからデータを受け取り、UI スレッドにデータを表示することができます。別のスレッドを使用してコレクションを変更することで、ユーザー インターフェイスはユーザーの操作に反応したままになります。

INotifyPropertyChanged によって発生した値の変更は、自動的にディスパッチャにマーシャリングされて戻されます。

クロススレッドアクセスを有効にする方法

覚えておいてください。 async メソッド自体はメインスレッドで実行されます。ですから、これは有効です。

private async void MyButton_ClickAsync(object sender, RoutedEventArgs e)
{
    txt.Text = "starting"; // UI Thread
    await Task.Run(()=> ExecuteLongProcedure1());
    txt.Text = "waiting"; // UI Thread
    await Task.Run(()=> ExecuteLongProcedure2());
    txt.Text = "finished"; // UI Thread
}

UI スレッドから UI 操作を呼び出すもう一つの方法として SynchronizationContext のように ここで . SynchronizationContext よりも強い抽象化です。 Dispatcher よりも強力な抽象化であり、クロスプラットフォームである。

var uiContext = SynchronizationContext.Current;
while (stillWorking)
{
    uiContext.Post(o =>
    {
        textBox.Text += ".";
    }, null);
    await Task.Delay(51);
}

パターン

ファイア・アンド・フォーゲット・パターン

明らかな理由により、これは、WPFのGUIイベントハンドラである Button_ClickAsync のような WPF GUI イベントハンドラが呼び出されます。

void Do()
{
    // CPU-Bound or IO-Bound operations
}
async void DoAsync() // returns void
{
    await Task.Run(Do);
}
void FireAndForget() // not blocks, not waits
{
    DoAsync();
}

火をつけて観察する

処理されない例外が発生した場合、その例外を処理するために TaskScheduler.UnobservedTaskException .

void Do()
{
    // CPU-Bound or IO-Bound operations
}
async Task DoAsync() // returns Task
{
    await Task.Run(Do);
}
void FireAndWait() // not blocks, not waits
{
    Task.Run(DoAsync);
}

スレッドリソースを浪費しながら同期的に発火・待機する。

これは、以下のように知られています。 非同期より同期 これは同期処理ですが、複数のスレッドを使用するため、飢餓状態に陥る可能性があります。この現象は Wait() から直接結果を読み込もうとしたり task.Result から直接結果を読み取ろうとします。

( このパターンを避ける )

void Do()
{
    // CPU-Bound or IO-Bound operations
}
async Task DoAsync() // returns Task
{
    await Task.Run(Do);
}
void FireAndWait() // blocks, waits and uses 2 more threads. Yikes!
{
    var task = Task.Run(DoAsync);
    task.Wait();
}


これで終わりですか?

いいえ、まだまだ学ぶべきことはたくさんあります。 async は、その コンテキスト とその 継続 . これは ブログ記事 は特におすすめです。

TaskがThreadを使う?確かですか?

そうとは限りません。読む この回答 の素顔をもっと知るために async .

スティーブン・クリアリー は、次のように説明しています。 async-await を完璧に説明しています。彼はまた、自分の 他のブログ記事 で説明しています。

もっと読む

ValueTaskとTask

MSDNでの説明 Task

MSDN の説明 async

同期メソッドから非同期メソッドを呼び出す方法

async await - 舞台裏

async await - よくある質問

非同期、並列、並行の違いを確認してください。

また、以下の記事もご覧ください。 シンプルな非同期ファイルライター を読んで、どこで同時進行すべきかを知ってください。

調査する コンカレント名前空間

最終的には、この電子書籍を読んでください。 並列プログラミングのパターン_CSharp