web-dev-qa-db-ja.com

WPF GUIから非同期タスクを実行および操作する方法

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 burnTheBaby = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);

        await burnTheBaby;
    }

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

これは実行されますが、ワーカーメソッドが開始されると、WPF関数ウィンドウは引き続きブロックされます。

Async/await/task宣言をどのように調整して許可するかを知る必要があります

A)GUIウィンドウをブロックしないワーカーメソッド
B)ワーカーメソッドにGUIウィンドウを更新させます
C)GUIウィンドウで割り込みを停止し、ワーカーメソッドを停止できるようにする

どんな助けやポインタも大歓迎です。

40
Kickaha

長い話:

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

    // wait for the task to finish, but don't block the UI thread
    await Task.Run(()=> HeavyMethod(txt));
    // The task is now completed.

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

// Running the Task causes this method to be executed in Thread Pool
internal void HeavyMethod(TextBox /*or any Control or Window*/ txt)
{
    while (stillWorking)
    {
        txt/*or a control or a window*/.Dispatcher.Invoke(() =>
        {
            // UI operations go inside of Invoke
            txt.Text += ".";
        });

        // CPU-bound or I/O-bound operations go outside of Invoke
        System.Threading.Thread.Sleep(51);
    }
}
Result:
txt.Text == "started....................done"

説明:

  1. awaitメソッドで使用できるのはasyncのみです。

  2. awaitのみawaitableオブジェクト(つまり、TaskまたはTask<T>)を使用できます

  3. Task.Run通常スレッドプールTaskをキューに入れます(つまり、スレッドプールの既存のスレッドを使用するか、スレッドプールに新しいスレッドを作成してタスクを実行します。これはすべて当てはまります。非同期操作が pure 操作でない場合、スレッドはなく、OSとデバイスドライバーによって処理される純粋な非同期操作だけです)

  4. awaitキーワードの魔法の能力のために、メインスレッドをブロックせずに、タスクは完了するまでasyncで待機し、結果を返します。

  5. asyncキーワードのmagicは、not別のスレッドを作成することです。コンパイラーがあきらめるおよび取り戻すだけでそのメソッドを制御できるようにします。 (メソッドとasyncキーワードを、Taskで囲まれたメソッドと混同しないでください

そう

あなたのメインスレッドは、通常のメソッドのようにasyncメソッド(MyButton_Click)を呼び出し、これまでスレッド化を行いません...これで、MyButton_Click内でタスクを実行できます:

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    //queue a task to run on threadpool
    Task task = Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
    //wait for it to end without blocking the main thread
    await task;
}

または単に

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
}

またはExecuteLongProcedurestring型の戻り値を持っている場合

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    Task<string> task = Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
    string returnValue = await task;
}

または単に

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    string returnValue = await Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));

    //or in cases where you already have a "Task returning" method:
    //  var httpResponseInfo = await httpRequestInfo.GetResponseAsync();
}

タスク内のメソッド(またはExecuteLongProcedure)は非同期的にを実行し、次のようになります。

//change the value for the following flag to terminate the loop
bool stillWorking = true;

//calling this method blocks the calling thread
//you must run a task for it
internal void ExecuteLongProcedure(MainWindow gui, int param1, int param2, int param3)
{
    //Start doing work
    gui.UpdateWindow("Work Started");

    while (stillWorking)
    {
        //put a dot in the window showing the progress
        gui.UpdateWindow(".");
        //the following line will block main thread unless
        //ExecuteLongProcedure is called with await keyword
        System.Threading.Thread.Sleep(51);
    }

    gui.UpdateWindow("Done and Done");
} 

戻りタイプ:

taskTask<T>型の場合、await taskステートメントによって返される値は、T型の値です。 taskTask型の場合、await taskは何も返しません(またはvoidを返します)。この時点で、コンパイラにawaitにタスクを終了するよう指示するか、次の行に進むことができます。

したがって、asyncメソッドが何も返さない場合は、async void MyMethod()またはasync Task MyMethod()を記述できます。また、asyncメソッドが何か(整数など)を返している場合は、async Task<int> MyMethodを記述できます。この場合、コードは次のようになります。

private async Task<int> MyMethod()
{
    int number = await Task.Run(todo);
    return number;
}

結果を待ちたくない場合の場合、非同期メソッド戻り値のタイプとしてTaskはおそらく必要ないため、これは明らかです。ただし、結果を待ちたい場合は、awaitasyncメソッドの結果そのメソッド内で行ったのと同じ方法である必要があります。例えばvar asyncResult = await MyMethod()

まだ混乱していますか? [〜#〜] msdn [〜#〜] で非同期戻り型を読み取ります。

注意:

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

awaitnotTask.Wait()です

ブロッキング:

SleepなどのCPUバインドまたはIOバインド操作は、asyncキーワードを使用したメソッドで呼び出された場合でも、メインスレッドをblockします。 (再び、asyncメソッドをTask内のメソッドと混同しないでください。非同期メソッド自体がタスクとして実行される場合、これは明らかに正しくありません:await MyAsyncMethod

await防止は、コンパイラがasyncメソッドに対する制御を放棄するため、メインスレッドをブロックしません。

private async void Button_Click(object sender, RoutedEventArgs e)
{
        Thread.Sleep(1000);//blocks
        await Task.Run(() => Thread.Sleep(1000));//does not block
}

WPF GUI:

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

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

ただし、ViewModelからのproperty changed callbackの結果としてタスクが開始された場合、コールバックは実際にUIスレッドから実行されるため、Dispatcher.Invokeを使用する必要はありません。

非UIスレッドのコレクションへのアクセス

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

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

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

asyncメソッド自体はメインスレッドで実行されることに注意してください。これは有効です:

private async void MyButton_Click(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
}

命名規則

戻り値の型がTaskまたはTask<T>のメソッド名にAsyncを後置するだけです。例えば:

Task WriteToFileAsync(string fileName)
{
    return Task.Run(()=>WriteToFile(fileName));
}
async void DoJob()
{
    await WriteToFileAsync("a.txt");
}
void Main()
{
    DoJob();
}

Task.Run()に渡されるメソッドには、Async接尾辞を使用しないでください。

個人的には、Async接尾辞をnotTaskまたはTask<T>を返すメソッドに使用すべきではないと思います。ただし、ほとんどの人は、asyncメソッドでこのプレフィックスを使用します。

これですべてですか?

いいえ。async、そのcontext、および継続について学ぶべきことはまだたくさんあります。

タスクはスレッドを使用しますか?本気ですか?

必ずしも。 この回答 を読んで、asyncの正体について詳しく知ることができます。

Stephen Clearyasync-awaitを完全に説明しました。また、彼は 他のブログ投稿 スレッドが関与していない場合について説明しています。

続きを読む

ValueTaskとTask

MSDNがTaskについて説明します

MSDNがasyncについて説明します

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

async await-舞台裏

async await-FAQ

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

また、 単純な非同期ファイルライターを読むこともできます どこで並行すべきかを知るため。

調査 同時ネームスペース

最終的に、この電子書籍をお読みください: Patterns_of_Parallel_Programming_CSharp

73
Bizhan

TaskCompletionSource<T>の使用は正しくありません。 TaskCompletionSource<T>は、非同期操作用の TAP互換ラッパー を作成する方法です。 ExecuteLongProcedureAsyncメソッドでは、サンプルコードはすべてCPUにバインドされています(つまり、本質的に同期ではなく、非同期です)。

したがって、ExecuteLongProcedureを同期メソッドとして記述する方がはるかに自然です。また、標準の動作に標準タイプを使用することをお勧めします。特に、 進捗状況の更新にIProgress<T>を使用する および CancellationTokenキャンセルの場合

internal void ExecuteLongProcedure(int param1, int param2, int param3,
    CancellationToken cancellationToken, IProgress<string> progress)
{       
  //Start doing work
  if (progress != null)
    progress.Report("Work Started");

  while (true)
  {
    //Mid procedure progress report
    if (progress != null)
      progress.Report("Bath water n% thrown out");
    cancellationToken.ThrowIfCancellationRequested();
  }

  //Exit message
  if (progress != null)
    progress.Report("Done and Done");
}

これで、適切な規則を使用する、より再利用可能なタイプ(GUI依存関係なし)ができました。次のように使用できます。

public partial class MainWindow : Window
{
  readonly otherClass _burnBabyBurn = new OtherClass();
  CancellationTokenSource _stopWorkingCts = new CancellationTokenSource();

  //A button method to start the long running method
  private async void Button_Click_3(object sender, RoutedEventArgs e)
  {
    var progress = new Progress<string>(data => UpdateWindow(data));
    try
    {
      await Task.Run(() => _burnBabyBurn.ExecuteLongProcedure(intParam1, intParam2, intParam3,
          _stopWorkingCts.Token, progress));
    }
    catch (OperationCanceledException)
    {
      // TODO: update the GUI to indicate the method was canceled.
    }
  }

  //A button Method to interrupt and stop the long running method
  private void StopButton_Click(object sender, RoutedEventArgs e)
  {
    _stopWorkingCts.Cancel();
  }

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

これは、Bijanによる最も一般的な回答の簡略版です。私は、Bijanの答えを単純化して、Stack Overflowが提供するNice形式を使用して問題を考えるのを助けました。

Bijanの投稿を注意深く読んで編集することで、ようやく理解できました。 非同期メソッドが完了するのを待つ方法

私の場合、その他の投稿に選ばれた答えは、最終的に私の問題を解決することにつながったものです:

async voidを避けます。メソッドがTaskの代わりにvoidを返すようにします。その後、awaitを使用できます。」

Bijanの(優れた)答えの私の簡易版は次のとおりです:

1)これは、asyncおよびawaitを使用してタスクを開始します。

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    // if ExecuteLongProcedureAsync has a return value
    var returnValue = await Task.Run(()=>
        ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));
}

2)これは非同期に実行する方法です:

bool stillWorking = true;
internal void ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
{
    //Start doing work
    gui.UpdateWindow("Work Started");

    while (stillWorking)
    {
        //put a dot in the window showing the progress
        gui.UpdateWindow(".");

        //the following line blocks main thread unless
        //ExecuteLongProcedureAsync is called with await keyword
        System.Threading.Thread.Sleep(50);
    }

    gui.UpdateWindow("Done and Done");
} 

3)GUIのプロパティを含む操作を呼び出します。

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

または、

void UpdateWindow(string text)
{
    //simply
    txt.Text += text;
}

最後のコメント)ほとんどの場合、2つの方法があります。

  • 最初のメソッド(Button_Click_3)は2番目のメソッドを呼び出し、そのメソッドのスレッド化を有効にするようコンパイラーに指示するasync修飾子を持っています。

    • asyncメソッドのThread.Sleepは、メインスレッドをブロックします。しかし、タスクを待つことはしません。
    • タスクが完了するまで、awaitステートメントの現在のスレッド(2番目のスレッド)で実行が停止します。
    • awaitメソッドの外でasyncを使用することはできません
  • 2番目のメソッド(ExecuteLongProcedureAsync)はタスク内にラップされ、awaitを前に追加することで非同期的に処理するように指示できる汎用Task<original return type>オブジェクトを返します。

    • このメソッドのすべてが非同期に実行されます

重要:

Lieroは重要な問題を提起しました。要素をViewModelプロパティにバインドすると、プロパティが変更されたコールバックがUIスレッドで実行されます。したがって、Dispatcher.Invokeを使用する必要はありません。 INotifyPropertyChangedによって発生した値の変更は、自動的にディスパッチャーにマーシャリングされます。

4
Eric D

async/awaitIProgress<T>およびCancellationTokenSourceを使用した例を次に示します。これらは、あなたがshouldを使用している最新のC#および.Net Framework言語機能です。他の解決策は私の目を少し出血させています。

コード機能

  • 10秒間で100までカウントする
  • 進行状況バーに進行状況を表示する
  • UIをブロックせずに実行される長時間実行作業(「待機」期間)
  • ユーザーがキャンセルをトリガー
  • 漸進的な進捗の更新
  • 操作後ステータスレポート

景色

<Window x:Class="ProgressExample.MainWindow"
        xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.Microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight" Height="93.258" Width="316.945">
    <StackPanel>
        <Button x:Name="Button_Start" Click="Button_Click">Start</Button>
        <ProgressBar x:Name="ProgressBar_Progress" Height="20"  Maximum="100"/>
        <Button x:Name="Button_Cancel" IsEnabled="False" Click="Button_Cancel_Click">Cancel</Button>
    </StackPanel>
</Window>

コード

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private CancellationTokenSource currentCancellationSource;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            // Enable/disabled buttons so that only one counting task runs at a time.
            this.Button_Start.IsEnabled = false;
            this.Button_Cancel.IsEnabled = true;

            try
            {
                // Set up the progress event handler - this instance automatically invokes to the UI for UI updates
                // this.ProgressBar_Progress is the progress bar control
                IProgress<int> progress = new Progress<int>(count => this.ProgressBar_Progress.Value = count);

                currentCancellationSource = new CancellationTokenSource();
                await CountToOneHundredAsync(progress, this.currentCancellationSource.Token);

                // Operation was successful. Let the user know!
                MessageBox.Show("Done counting!");
            }
            catch (OperationCanceledException)
            {
                // Operation was cancelled. Let the user know!
                MessageBox.Show("Operation cancelled.");
            }
            finally
            {
                // Reset controls in a finally block so that they ALWAYS go 
                // back to the correct state once the counting ends, 
                // regardless of any exceptions
                this.Button_Start.IsEnabled = true;
                this.Button_Cancel.IsEnabled = false;
                this.ProgressBar_Progress.Value = 0;

                // Dispose of the cancellation source as it is no longer needed
                this.currentCancellationSource.Dispose();
                this.currentCancellationSource = null;
            }
        }

        private async Task CountToOneHundredAsync(IProgress<int> progress, CancellationToken cancellationToken)
        {
            for (int i = 1; i <= 100; i++)
            {
                // This is where the 'work' is performed. 
                // Feel free to swap out Task.Delay for your own Task-returning code! 
                // You can even await many tasks here

                // ConfigureAwait(false) tells the task that we dont need to come back to the UI after awaiting
                // This is a good read on the subject - https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
                await Task.Delay(100, cancellationToken).ConfigureAwait(false);

                // If cancelled, an exception will be thrown by the call the task.Delay
                // and will bubble up to the calling method because we used await!

                // Report progress with the current number
                progress.Report(i);
            }
        }

        private void Button_Cancel_Click(object sender, RoutedEventArgs e)
        {
            // Cancel the cancellation token
            this.currentCancellationSource.Cancel();
        }
    }
3
Gusdor