2013-09-26 212 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並沒有要求如何等待一切完成,他們希望在任務運行時更新標籤,而不是之後。 –