在WinForms上使用async / await访问Task.Run中的UI控件

我在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() ,你希望代码在当前上下文中运行,所以这正是发生的事情。

但是没有必要在代码中使用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并在任务完成后继续执行捕获的上下文。

因此,在您的情况下,您有一个嵌套的await ,它位于Task.Run因此第二个await将捕获不会是UiSynchronizationContext的上下文,因为它正在由ThreadPoolWorkerThread执行。

这回答了你的问题吗?

试试这个:

更换

 label1.Text = "test"; 

 SetLabel1Text("test"); 

并将以下内容添加到您的类:

 private void SetLabel1Text(string text) { if (InvokeRequired) { Invoke((Action)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/

我将给你我最新的答案,我已经给出了异步理解。

解决方法就像您所知,当您调用异步方法时,您需要作为任务运行。

这是一个快速的控制台应用程序代码,您可以使用它作为参考,它将使您易于理解这个概念。

 using System; using System.Threading; using System.Threading.Tasks; public class Program { public static void Main() { Console.WriteLine("Starting Send Mail Async Task"); Task task = new Task(SendMessage); task.Start(); Console.WriteLine("Update Database"); UpdateDatabase(); while (true) { // dummy wait for background send mail. if (task.Status == TaskStatus.RanToCompletion) { break; } } } public static async void SendMessage() { // Calls to TaskOfTResult_MethodAsync Task returnedTaskTResult = MailSenderAsync(); bool result = await returnedTaskTResult; if (result) { UpdateDatabase(); } Console.WriteLine("Mail Sent!"); } private static void UpdateDatabase() { for (var i = 1; i < 1000; i++) ; Console.WriteLine("Database Updated!"); } private static async Task MailSenderAsync() { Console.WriteLine("Send Mail Start."); for (var i = 1; i < 1000000000; i++) ; return true; } } 

在这里,我尝试启动称为发送邮件的任务。 临时我想更新数据库,而后台正在执行发送邮件任务。

数据库更新发生后,它正在等待发送邮件任务完成。 但是,使用这种方法很明显我可以在后台运行任务并继续使用原始(主)线程。