2013-09-26 214 views
17

我有一个按钮和一个标签WinForms应用程序以下代码:访问UI控件在Task.Run与异步/等待上的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"; 
      });  
     } 
    } 
} 

这是实际应用I”的一个简化的版本正在努力。我的印象是,通过在我的Task.Run中使用async/await,我可以设置label1.Text属性。但是,运行此代码时,我得到错误信息,说明我不在UI线程中,并且无法访问该控件。

为什么我不能访问标签控件?

回答

6

为什么你使用Task.Run?启动一个新的工作线程(CPU绑定),它会导致你的问题。

你或许应该只是这样做:

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

等待确保您将继续在同样的情况下,除非您使用.ConfigureAwait(假);

13

试试这个

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   
} 

异步/等待并不能保证它会在UI线程中运行。 await将捕获当前SynchronizationContext,并在任务完成后继续使用捕获的上下文执行。

所以你的情况,你有一个嵌套await这里面Task.Run因此第二await将捕获它不会是UiSynchronizationContext,因为它正由WorkerThreadThreadPool执行上下文。

这是否回答您的问题?

21

当你使用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"; 
} 
+3

您不应该使用GUI同步上下文中的异步无效方法。 C.F. http://msdn.microsoft.com/en-us/magazine/jj991977.aspx - “GUI和ASP.NET应用程序有一个SynchronizationContext,一次只允许运行一个代码块。当await完成时,它尝试在捕获的上下文中执行异步方法的其余部分,但该上下文已经有一个线程,它正在(同步)等待异步方法完成,它们都在等待另一个,导致死锁。 – Daniel

+10

@Daniel这个引用是关于异步代码的同步等待(例如'Wait()'),我当然不会这么说。从同一篇文章:“无效返回异步方法有一个特定的目的:使异步事件处理程序成为可能。”这正是我在这里做的。 – svick

+2

值得注意的是,任何未处理的私有异步任务运行()方法中发生的异常都会“冒泡”到异步事件处理程序本身。 – Jaans

6

试试这个:

更换

label1.Text = "test"; 

SetLabel1Text("test"); 

和添加以下内容到你的类:如果你不是在UI线程

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

的InvokeRequired返回true。 Invoke()方法接受委托和参数,切换到UI线程,然后递归调用方法。您在Invoke()调用之后返回,因为该方法在Invoke()返回之前已经被递归调用。如果在调用方法时碰巧在UI线程上,则InvokeRequired为false,并且直接执行分配。

-1

我打算给你我最近给出的异步理解答案。

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

这是一个快速控制台应用程序代码,您可以使用它作为参考,它会让您轻松理解概念。

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<bool> 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<bool> MailSenderAsync() 
    { 
     Console.WriteLine("Send Mail Start."); 
     for (var i = 1; i < 1000000000; i++) ; 
     return true; 
    } 
} 

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

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

+2

我永远不会对需要时间写回答​​的人进行投票,但我确实认为'while(true)'可能会占用CPU资源,使得任务花费的时间要比其他方式长。 – jp2code

+0

这不会更新UI线程作为正在进行的步骤。 OP并没有要求如何等待一切完成,他们希望在任务运行时更新标签,而不是之后。 –