2013-09-26 55 views
17

Tôi có đoạn mã sau trong một ứng dụng WinForms với một nút và một label:Truy cập vào giao diện điều khiển trong Task.Run với async/đang chờ đợi trên 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"; 
      });  
     } 
    } 
} 

Đây là một phiên bản đơn giản của ứng dụng thực tế tôi m làm việc trên. Tôi đã có ấn tượng rằng bằng cách sử dụng async/await trong số Task.Run tôi có thể đặt thuộc tính label1.Text. Tuy nhiên, khi chạy mã này tôi nhận được lỗi rằng tôi không phải trên thread UI và tôi không thể truy cập vào điều khiển.

Tại sao tôi không thể truy cập điều khiển nhãn?

Trả lời

6

Tại sao bạn sử dụng Task.Run? bắt đầu một chuỗi công nhân mới (ràng buộc CPU), và nó gây ra vấn đề của bạn.

bạn nên có lẽ chỉ làm điều đó:

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

chờ đợi chắc chắn rằng bạn sẽ tiếp tục bối cảnh tương tự, ngoại trừ nếu bạn sử dụng .ConfigureAwait (false);

13

Hãy thử điều này

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

Hoặc

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

Hoặc

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/chờ đợi không đảm bảo rằng nó sẽ chạy trong thread UI. await sẽ chụp SynchronizationContext hiện tại và tiếp tục thực hiện với ngữ cảnh đã chụp khi tác vụ hoàn tất.

Vì vậy, trong trường hợp của bạn, bạn có một lồng nhau await mà là bên trong Task.Run vì thế thứ hai await sẽ nắm bắt được bối cảnh đó sẽ không được UiSynchronizationContext vì nó đang được thực hiện bởi WorkerThread từ ThreadPool.

Điều này có trả lời câu hỏi của bạn không?

21

Khi bạn sử dụng Task.Run(), bạn đang saing rằng bạn không muốn mã chạy trên ngữ cảnh hiện tại, vì vậy đó chính xác là những gì sẽ xảy ra.

Nhưng không cần sử dụng Task.Run() trong mã của bạn. Các phương thức async được viết chính xác sẽ không chặn luồng hiện tại, vì vậy bạn có thể sử dụng chúng trực tiếp từ chuỗi giao diện người dùng. Nếu bạn làm điều đó, await sẽ đảm bảo phương thức tiếp tục trở lại trên chuỗi giao diện người dùng.

Điều này có nghĩa rằng nếu bạn viết mã của bạn như thế này, nó sẽ làm việc:

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

Bạn không nên sử dụng các phương thức void không đồng bộ từ ngữ cảnh đồng bộ hóa GUI. C.f. http://msdn.microsoft.com/en-us/magazine/jj991977.aspx - "Các ứng dụng GUI và ASP.NET có một SynchronizationContext cho phép chỉ một đoạn mã chạy cùng một lúc. Khi chờ đợi hoàn thành, nó sẽ cố gắng để thực thi phần còn lại của phương thức async trong bối cảnh đã capture, nhưng ngữ cảnh đó đã có một luồng trong đó, đó là (đồng bộ) chờ đợi phương thức async hoàn tất. Chúng đang đợi nhau, gây ra bế tắc. " – Daniel

+10

@Daniel Trích dẫn đó là về việc chờ đồng bộ (ví dụ: 'Chờ()') trên mã không đồng bộ và tôi chắc chắn không đề xuất điều đó. Từ cùng một bài báo: “Các phương thức không đồng bộ hóa ngược trở lại có một mục đích cụ thể: để tạo các trình xử lý sự kiện không đồng bộ có thể.” Và đó chính xác là những gì tôi đang làm ở đây. – svick

+2

Cần lưu ý rằng bất kỳ ngoại lệ nào xảy ra trong phương thức 'private async Task Run()' không được xử lý, sẽ "bong bóng" lên đến trình xử lý sự kiện không đồng bộ. – Jaans

6

Hãy thử điều này:

thay

label1.Text = "test"; 

với

SetLabel1Text("test"); 

và thêm thông tin sau vào lớp học:

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

InvokeRequired trả về true nếu bạn KHÔNG ở trên chuỗi giao diện người dùng. Phương thức Invoke() nhận ủy nhiệm và tham số, chuyển sang luồng UI và sau đó gọi phương thức đệ quy. Bạn trở lại sau lời gọi Invoke() vì phương thức này đã được gọi đệ quy trước khi Invoke() trở về. Nếu bạn tình cờ gặp thread UI khi phương thức được gọi, InvokeRequired là false và phép gán được thực hiện trực tiếp.

-1

Tôi sẽ cung cấp cho bạn câu trả lời mới nhất mà tôi đã cung cấp cho sự hiểu biết không đồng bộ.

Giải pháp là như bạn biết khi bạn đang gọi phương thức không đồng bộ, bạn cần chạy dưới dạng tác vụ.

Dưới đây là mã ứng dụng bảng điều khiển nhanh mà bạn có thể sử dụng để tham khảo, nó sẽ giúp bạn dễ dàng hiểu khái niệm này.

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; 
    } 
} 

Ở đây tôi đang cố bắt đầu tác vụ được gọi là gửi thư. Tạm thời tôi muốn cập nhật cơ sở dữ liệu, trong khi nền đang thực hiện tác vụ gửi thư.

Sau khi cập nhật cơ sở dữ liệu đã xảy ra, nó đang chờ hoàn thành nhiệm vụ gửi thư. Tuy nhiên, với cách tiếp cận này, nó khá rõ ràng là tôi có thể chạy tác vụ ở nền và vẫn tiếp tục với luồng gốc (chính).

+2

Tôi sẽ không bao giờ bỏ phiếu bầu ai đó dành thời gian để viết câu trả lời, nhưng tôi nghĩ 'while (true) 'có thể hog tài nguyên CPU, khiến nhiệm vụ mất nhiều thời gian hơn nếu không. – jp2code

+0

Điều này không cập nhật chuỗi giao diện người dùng làm bước liên tục. OP không yêu cầu cách đợi cho đến khi mọi thứ được thực hiện, họ muốn cập nhật nhãn trong khi Tác vụ đang chạy, không phải sau đó. –

Các vấn đề liên quan