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スレッド上にないためコントロールにアクセスできないというエラーが表示されます。
ラベルコントロールにアクセスできないのはなぜですか?
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";
}
これを試して
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番目のawait
はUiSynchronizationContext
にならないコンテキストをキャプチャします。 WorkerThread
からThreadPool
によって実行されます。
これはあなたの質問に答えますか?
これを試して:
取り替える
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であり、割り当てが直接実行されます。
なぜTask.Runを使用するのですか?新しいワーカースレッド(CPUバウンド)を開始すると、問題が発生します。
あなたはおそらくそれをするべきです:
private async Task Run()
{
await File.AppendText("temp.dat").WriteAsync("a");
label1.Text = "test";
}
.ConfigureAwait(false);を使用する場合を除き、同じコンテキストで続行することを確認してください。
別のスレッド上にあり、クロススレッド呼び出しは許可されていないためです。
開始するスレッドに「コンテキスト」を渡す必要があります。こちらの例をご覧ください: http://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/