2009-12-16 29 views
31

Tôi đang viết một chương trình Visual C# thực hiện một vòng lặp liên tục của các hoạt động trên một chủ đề thứ cấp. Thỉnh thoảng khi chủ đề đó kết thúc một tác vụ tôi muốn nó kích hoạt một eventhandler. Chương trình của tôi thực hiện điều đó nhưng khi trình xử lý sự kiện được kích hoạt, luồng thứ hai chờ cho đến khi trình xử lý sự kiện kết thúc trước khi tiếp tục luồng. Làm thế nào để tôi tiếp tục? Đây là cách tôi hiện đang có cấu trúc ...Làm cách nào để làm cho trình xử lý sự kiện chạy không đồng bộ?

class TestClass 
{ 
    private Thread SecondaryThread; 
    public event EventHandler OperationFinished; 

    public void StartMethod() 
    { 
    ... 
    SecondaryThread.Start();  //start the secondary thread 
    } 

    private void SecondaryThreadMethod() 
    { 
    ... 
    OperationFinished(null, new EventArgs()); 
    ... //This is where the program waits for whatever operations take 
     //place when OperationFinished is triggered. 
    } 

} 

Mã này là một phần của API cho một trong các thiết bị của tôi. Khi sự kiện OperationFinished được kích hoạt, tôi muốn ứng dụng khách có thể làm bất cứ điều gì cần thiết (tức là cập nhật GUI cho phù hợp) mà không làm hỏng hoạt động API.

Ngoài ra, nếu tôi không muốn chuyển bất kỳ tham số nào cho trình xử lý sự kiện, cú pháp của tôi có đúng không bằng cách sử dụng OperationFinished(null, new EventArgs())?

+0

Những chủ đề nào bạn muốn các 'sự kiện OperationFinished' được nâng lên trên? Nó không thể là chuỗi thứ cấp của bạn, vì bạn yêu cầu không chặn nó một cách rõ ràng. Liệu nó có phải là chủ đề chính, sau đó, hoặc là bạn tốt với nó được đưa ra trên một chủ đề khác nhau mới được tạo ra chỉ với mục đích gọi lại async? –

Trả lời

44

Vì vậy, bạn muốn tăng sự kiện theo cách ngăn người nghe chặn luồng nền? Gimme một vài phút để roi lên một ví dụ; nó khá đơn giản :-)

Ở đây chúng ta đi: đầu tiên là một lưu ý quan trọng! Bất cứ khi nào bạn gọi BeginInvoke, bạn phải gọi EndInvoke tương ứng, nếu không, phương thức được gọi đã ném một ngoại lệ hoặc trả lại giá trị thì luồng ThreadPool sẽ không bao giờ được trả lại vào hồ bơi, dẫn đến rò rỉ luồng!

class TestHarness 
{ 

    static void Main(string[] args) 
    { 
     var raiser = new SomeClass(); 

     // Emulate some event listeners 
     raiser.SomeEvent += (sender, e) => { Console.WriteLine(" Received event"); }; 
     raiser.SomeEvent += (sender, e) => 
     { 
      // Bad listener! 
      Console.WriteLine(" Blocking event"); 
      System.Threading.Thread.Sleep(5000); 
      Console.WriteLine(" Finished blocking event"); 
     }; 

     // Listener who throws an exception 
     raiser.SomeEvent += (sender, e) => 
     { 
      Console.WriteLine(" Received event, time to die!"); 
      throw new Exception(); 
     }; 

     // Raise the event, see the effects 
     raiser.DoSomething(); 

     Console.ReadLine(); 
    } 
} 

class SomeClass 
{ 
    public event EventHandler SomeEvent; 

    public void DoSomething() 
    { 
     OnSomeEvent(); 
    } 

    private void OnSomeEvent() 
    { 
     if (SomeEvent != null) 
     { 
      var eventListeners = SomeEvent.GetInvocationList(); 

      Console.WriteLine("Raising Event"); 
      for (int index = 0; index < eventListeners.Count(); index++) 
      { 
       var methodToInvoke = (EventHandler)eventListeners[index]; 
       methodToInvoke.BeginInvoke(this, EventArgs.Empty, EndAsyncEvent, null); 
      } 
      Console.WriteLine("Done Raising Event"); 
     } 
    } 

    private void EndAsyncEvent(IAsyncResult iar) 
    { 
     var ar = (System.Runtime.Remoting.Messaging.AsyncResult)iar; 
     var invokedMethod = (EventHandler)ar.AsyncDelegate; 

     try 
     { 
      invokedMethod.EndInvoke(iar); 
     } 
     catch 
     { 
      // Handle any exceptions that were thrown by the invoked method 
      Console.WriteLine("An event listener went kaboom!"); 
     } 
    } 
} 
+2

Tại sao không chỉ gọi trực tiếp cho đại biểu multicast, thay vì sử dụng GetInvocationList? – thecoop

+1

Làm cách nào để bạn gọi trình xử lý sự kiện không đồng bộ chỉ bằng cách sử dụng? Cấp, bạn có thể gọi * tất cả * người nghe trên một chủ đề riêng biệt duy nhất - giải pháp của tôi đưa nó đến mức gọi * mỗi người nghe * theo chủ đề của riêng họ - vì vậy tôi có thể thấy nó quá mức cần thiết. – STW

+0

Cách tôi đã viết ban đầu của tôi, nếu không có phương pháp để xử lý sự kiện trong ứng dụng khách (không có người nghe) ứng dụng khách sẽ ném một ngoại lệ. Bạn có ngăn chặn điều đó xảy ra bằng cách sử dụng vòng lặp đó thông qua eventListeners không? – PICyourBrain

0

Nhìn vào lớp BackgroundWorker. Tôi nghĩ rằng nó chính xác những gì bạn đang yêu cầu.

EDIT: Điều tôi nghĩ bạn đang hỏi là cách kích hoạt sự kiện khi chỉ một phần nhỏ của tác vụ nền tổng thể hoàn tất. BackgroundWorker cung cấp một sự kiện được gọi là "ProgressChanged" cho phép bạn báo cáo lại chủ đề chính rằng một phần của quá trình tổng thể đã hoàn tất. Sau đó, khi tất cả công việc async hoàn tất, nó sẽ làm tăng sự kiện "RunWorkerCompleted".

+1

Không chắc cách BackgroundWorker giúp trong tình huống này. Cấp, đó là một lựa chọn tuyệt vời để đẩy công việc vào một chủ đề riêng biệt khi bạn cần thông báo, nhưng trong trường hợp này, nó chỉ là một mục công việc đơn giản để đẩy trình xử lý vào một chuỗi riêng ... –

+0

Nếu tôi viết ứng dụng khách, tôi có thể có phương thức cập nhật GUI chạy trong backgroundworker và điều đó sẽ dừng cuộc gọi đến OperationFinished() khỏi việc chặn, nhưng khi tôi không viết ứng dụng khách, tôi không thể làm điều đó. Bạn có nói rằng cuộc gọi của tôi tới OpeartionFinished() phải ở trong một người làm việc nền? – PICyourBrain

11

Ngoài ra, nếu tôi không muốn chuyển bất kỳ tham số nào cho trình xử lý sự kiện, cú pháp của tôi đúng bằng cách sử dụng OperationFinished (null, new EventArgs())?

số Thông thường, bạn sẽ gọi nó là:

OperationFinished(this, EventArgs.Empty); 

Bạn nên luôn luôn vượt qua một đối tượng như một người gửi - nó dự kiến ​​trong mô hình (mặc dù thường bị bỏ qua). EventArgs.Empty cũng tốt hơn EventArgs() mới.

Để bắn này trong một chủ đề riêng biệt, tùy chọn dễ dàng nhất có lẽ là để chỉ cần sử dụng hồ bơi thread:

private void RaiseOperationFinished() 
{ 
     ThreadPool.QueueUserWorkItem(new WaitCallback((s) => 
      { 
       if (this.OperationFinished != null) 
        this.OperationFinished(this, EventArgs.Empty); 
      })); 
} 

đó đang được nói, nâng cao một sự kiện trên một thread riêng biệt là cái gì đó nên được triệt để tài liệu, vì nó sẽ có khả năng gây ra hành vi bất ngờ.

+2

Hôm nay, tôi sẽ sử dụng 'Task.Run' thay vì nhóm luồng. – beruic

+1

@beruic Đồng ý. Điều này được viết vào năm 2009;) –

6

Thử phương thức BeginInvoke và EndInvoke trên ủy nhiệm sự kiện - những trở lại này ngay lập tức và cho phép bạn sử dụng bỏ phiếu, chờ xử lý hoặc chức năng gọi lại để thông báo cho bạn khi phương thức đã hoàn tất. Xem here để biết tổng quan; trong ví dụ của bạn, sự kiện là đại biểu bạn sẽ sử dụng

0

Tôi thích xác định phương thức mà tôi chuyển đến chuỗi con làm đại biểu cập nhật giao diện người dùng. Đầu tiên xác định một đại biểu:

public delegate void ChildCallBackDelegate(); 

Trong thread con xác định một thành viên đại biểu:

public ChildCallbackDelegate ChildCallback {get; set;} 

Trong lớp gọi điện thoại xác định phương pháp cập nhật giao diện người dùng. Bạn sẽ cần bọc nó trong bộ điều phối của điều khiển đích vì nó được gọi từ một luồng riêng biệt. Lưu ý BeginInvoke. Trong bối cảnh này EndInvoke không cần:

private void ChildThreadUpdater() 
{ 
    yourControl.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background 
    , new System.Threading.ThreadStart(delegate 
     { 
     // update your control here 
     } 
    )); 
} 

Trước khi bạn khởi động chủ đề con quý vị, thiết lập thuộc tính ChildCallBack của nó:

theChild.ChildCallBack = new ChildCallbackDelegate(ChildThreadUpdater); 

Sau đó, khi các sợi con muốn cập nhật phụ huynh:

ChildCallBack(); 
+0

Bạn có thể trích dẫn các nguồn để sao lưu mà 'EndInvoke()' là không cần thiết? Sự hiểu biết của tôi là nó luôn luôn thực hành tốt để đảm bảo nó được gọi là nguồn tài nguyên luồng không nhất thiết phải giải phóng mà không có cuộc gọi theo hoàn cảnh cụ thể. Ngoài ra, có một lý do bạn chọn sử dụng một ThreadStart hơn là (tương đối) performant ThreadPool? Cuối cùng; giải pháp này xử lý việc cập nhật giao diện người dùng, nhưng tôi không nghĩ rằng câu hỏi của OP bị giới hạn ở đó - nó không giải quyết được vấn đề lớn hơn về việc tăng sự kiện một cách không đồng bộ. – STW

+1

Jon Skeet nói rằng tốt nhất: http://stackoverflow.com/questions/229554/whats-the-difference-between-invoke-and-begininvoke: "Lưu ý rằng nhóm Windows Forms đã đảm bảo rằng bạn có thể sử dụng Control.BeginInvoke theo cách "cháy và quên" - nghĩa là không bao giờ gọi EndInvoke. Điều này không đúng với các cuộc gọi không đồng bộ nói chung: thông thường mỗi BeginXXX sẽ có một cuộc gọi EndXXX tương ứng, thường là trong cuộc gọi lại. " Cũng lưu ý rằng ít nhất với WPF, không có phương thức Dispatcher.EndInvoke. –

+0

Tôi đã cập nhật giải pháp giao diện người dùng vì đó là những gì OP đã chỉ định: "Khi sự kiện OperationFinished được kích hoạt, tôi muốn ứng dụng khách có thể thực hiện bất kỳ điều gì cần thiết (tức là cập nhật GUI tương ứng) mà không làm hỏng hoạt động API." –

8

Với Task Parallel Library, bạn có thể thực hiện các thao tác sau:

Task.Factory.FromAsync((asyncCallback, @object) => this.OperationFinished.BeginInvoke(this, EventArgs.Empty, asyncCallback, @object), this.OperationFinished.EndInvoke, null); 
+0

Hoạt động tốt, cảm ơn bạn đã nhắc đến phương thức 'FromAsync' của TPL! – NumberFour

+0

Thông minh, nhưng sẽ không hoạt động đối với các đại biểu đa phương tiện –

+1

@FactorMytic Bạn có biết nơi tôi có thể đọc thêm về lý do tại sao nó không hoạt động trong trường hợp đó? – piedar

3

lẽ Method2 hoặc Method3 dưới đây có thể giúp :)

public partial class Form1 : Form 
{ 
    private Thread SecondaryThread; 

    public Form1() 
    { 
     InitializeComponent(); 

     OperationFinished += callback1; 
     OperationFinished += callback2; 
     OperationFinished += callback3; 
    } 

    private void Form1_Load(object sender, EventArgs e) 
    { 
     SecondaryThread = new Thread(new ThreadStart(SecondaryThreadMethod)); 
     SecondaryThread.Start(); 
    } 

    private void SecondaryThreadMethod() 
    { 
     Stopwatch sw = new Stopwatch(); 
     sw.Restart(); 

     OnOperationFinished(new MessageEventArg("test1")); 
     OnOperationFinished(new MessageEventArg("test2")); 
     OnOperationFinished(new MessageEventArg("test3")); 
     //This is where the program waits for whatever operations take 
      //place when OperationFinished is triggered. 

     sw.Stop(); 

     Invoke((MethodInvoker)delegate 
     { 
      richTextBox1.Text += "Time taken (ms): " + sw.ElapsedMilliseconds + "\n"; 
     }); 
    } 

    void callback1(object sender, MessageEventArg e) 
    { 
     Thread.Sleep(2000); 
     Invoke((MethodInvoker)delegate 
     { 
      richTextBox1.Text += e.Message + "\n"; 
     }); 
    } 
    void callback2(object sender, MessageEventArg e) 
    { 
     Thread.Sleep(2000); 
     Invoke((MethodInvoker)delegate 
     { 
      richTextBox1.Text += e.Message + "\n"; 
     }); 
    } 

    void callback3(object sender, MessageEventArg e) 
    { 
     Thread.Sleep(2000); 
     Invoke((MethodInvoker)delegate 
     { 
      richTextBox1.Text += e.Message + "\n"; 
     }); 
    } 

    public event EventHandler<MessageEventArg> OperationFinished; 

    protected void OnOperationFinished(MessageEventArg e) 
    { 
     //##### Method1 - Event raised on the same thread ##### 
     //EventHandler<MessageEventArg> handler = OperationFinished; 

     //if (handler != null) 
     //{ 
     // handler(this, e); 
     //} 

     //##### Method2 - Event raised on (the same) separate thread for all listener ##### 
     //EventHandler<MessageEventArg> handler = OperationFinished; 

     //if (handler != null) 
     //{ 
     // Task.Factory.StartNew(() => handler(this, e)); 
     //} 

     //##### Method3 - Event raised on different threads for each listener ##### 
     if (OperationFinished != null) 
     { 
      foreach (EventHandler<MessageEventArg> handler in OperationFinished.GetInvocationList()) 
      { 
       Task.Factory.FromAsync((asyncCallback, @object) => handler.BeginInvoke(this, e, asyncCallback, @object), handler.EndInvoke, null); 
      } 
     } 
    } 
} 

public class MessageEventArg : EventArgs 
{ 
    public string Message { get; set; } 

    public MessageEventArg(string message) 
    { 
     this.Message = message; 
    } 
} 

}

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