web-dev-qa-db-ja.com

WinFormsでasync / awaitを使用してTask.RunのUIコントロールにアクセスする

1つのボタンと1つのラベルを持つWinFormsアプリケーションに次のコードがあります。

using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            await Run();
        }

        private async Task Run()
        {
            await Task.Run(async () => {
                await File.AppendText("temp.dat").WriteAsync("a");
                label1.Text = "test";
            });    
        }
    }
}

これは、私が取り組んでいる実際のアプリケーションの簡易バージョンです。 Task.Runでasync/awaitを使用すると、label1.Textプロパティを設定できるという印象を受けました。ただし、このコードを実行すると、UIスレッド上にないためコントロールにアクセスできないというエラーが表示されます。

ラベルコントロールにアクセスできないのはなぜですか?

23
Wouter de Kort

Task.Run()を使用すると、現在のコンテキストでコードを実行することをdo n'tにしたいので、まさにそれが起こります。

ただし、コードでTask.Run()を使用する必要はありません。正しく記述されたasyncメソッドは現在のスレッドをブロックしないため、UIスレッドから直接使用できます。その場合、awaitはメソッドがUIスレッドで再開することを確認します。

これは、次のようにコードを記述すれば機能することを意味します。

private async void button1_Click(object sender, EventArgs e)
{
    await Run();
}

private async Task Run()
{
    await File.AppendText("temp.dat").WriteAsync("a");
    label1.Text = "test";
}
26
svick

これを試して

private async Task Run()
{
    await Task.Run(async () => {
       await File.AppendText("temp.dat").WriteAsync("a");
       });
    label1.Text = "test";
}

または

private async Task Run()
{
    await File.AppendText("temp.dat").WriteAsync("a");        
    label1.Text = "test";
}

または

private async Task Run()
{
    var task = Task.Run(async () => {
       await File.AppendText("temp.dat").WriteAsync("a");
       });
    var continuation = task.ContinueWith(antecedent=> label1.Text = "test",TaskScheduler.FromCurrentSynchronizationContext());
    await task;//I think await here is redundant        
}

async/awaitは、UIスレッドで実行されることを保証しません。 awaitは現在のSynchronizationContextをキャプチャし、タスクが完了するとキャプチャされたコンテキストで実行を継続します。

したがって、あなたの場合、Task.Runの中にあるネストされたawaitがあるため、2番目のawaitUiSynchronizationContextにならないコンテキストをキャプチャします。 WorkerThreadからThreadPoolによって実行されます。

これはあなたの質問に答えますか?

14

これを試して:

取り替える

label1.Text = "test";

SetLabel1Text("test");

クラスに次を追加します。

private void SetLabel1Text(string text)
{
  if (InvokeRequired)
  {
    Invoke((Action<string>)SetLabel1Text, text);
    return;
  }
  label1.Text = text;
}

UIスレッドを使用していない場合、InvokeRequiredはtrueを返します。 Invoke()メソッドはデリゲートとパラメーターを受け取り、UIスレッドに切り替えてから、メソッドを再帰的に呼び出します。 Invoke()が戻る前にメソッドがすでに再帰的に呼び出されているため、Invoke()呼び出しの後に戻ります。メソッドが呼び出されたときにUIスレッド上にいる場合、InvokeRequiredはfalseであり、割り当てが直接実行されます。

10
Metro

なぜTask.Runを使用するのですか?新しいワーカースレッド(CPUバウンド)を開始すると、問題が発生します。

あなたはおそらくそれをするべきです:

    private async Task Run()
    {
        await File.AppendText("temp.dat").WriteAsync("a");
        label1.Text = "test";    
    }

.ConfigureAwait(false);を使用する場合を除き、同じコンテキストで続行することを確認してください。

6
Persi

別のスレッド上にあり、クロススレッド呼び出しは許可されていないためです。

開始するスレッドに「コンテキスト」を渡す必要があります。こちらの例をご覧ください: http://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/

0
Gerrie Schenck