C # Sự kiện và An toàn chủ đề


236

CẬP NHẬT

Kể từ C # 6, câu trả lời cho câu hỏi này là:

SomeEvent?.Invoke(this, e);

Tôi thường xuyên nghe / đọc lời khuyên sau:

Luôn tạo một bản sao của một sự kiện trước khi bạn kiểm tra nullvà kích hoạt nó. Điều này sẽ loại bỏ một vấn đề tiềm ẩn với việc xâu chuỗi nơi sự kiện trở thành nulltại địa điểm ngay giữa nơi bạn kiểm tra null và nơi bạn phát sinh sự kiện:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Đã cập nhật : Tôi nghĩ từ việc đọc về tối ưu hóa rằng điều này cũng có thể yêu cầu thành viên sự kiện biến động, nhưng Jon Skeet nói trong câu trả lời của mình rằng CLR không tối ưu hóa bản sao.

Nhưng trong khi đó, để vấn đề này thậm chí xảy ra, một luồng khác phải làm một cái gì đó như thế này:

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

Trình tự thực tế có thể là hỗn hợp này:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Vấn đề là OnTheEventchạy theo tác giả đã hủy đăng ký, và họ chỉ hủy đăng ký cụ thể để tránh điều đó xảy ra. Chắc chắn những gì thực sự cần thiết là một triển khai sự kiện tùy chỉnh với sự đồng bộ hóa phù hợp trong addvà các bộ truy cập remove. Và ngoài ra, có vấn đề về sự bế tắc có thể xảy ra nếu khóa được giữ trong khi một sự kiện được bắn.

Vì vậy, đây là lập trình Cult Cult ? Có vẻ như vậy - rất nhiều người phải thực hiện bước này để bảo vệ mã của họ khỏi nhiều luồng, trong thực tế, dường như đối với tôi, các sự kiện đòi hỏi nhiều sự quan tâm hơn trước khi chúng có thể được sử dụng như một phần của thiết kế đa luồng . Do đó, những người không thực hiện sự chăm sóc bổ sung đó cũng có thể bỏ qua lời khuyên này - đơn giản đó không phải là vấn đề đối với các chương trình đơn luồng, và trên thực tế, do không có volatilehầu hết mã ví dụ trực tuyến, lời khuyên có thể không có có tác dụng gì cả.

(Và không đơn giản hơn nhiều khi chỉ gán chỗ trống delegate { }trên tờ khai thành viên để bạn không bao giờ cần phải kiểm tra nullngay từ đầu?)

Cập nhật:Trong trường hợp không rõ ràng, tôi đã nắm được ý định của lời khuyên - để tránh ngoại lệ tham chiếu null trong mọi trường hợp. Quan điểm của tôi là ngoại lệ tham chiếu null đặc biệt này chỉ có thể xảy ra nếu một luồng khác bị hủy khỏi sự kiện và lý do duy nhất để làm điều đó là để đảm bảo rằng sẽ không nhận được cuộc gọi nào nữa thông qua sự kiện đó, điều này rõ ràng KHÔNG đạt được bằng kỹ thuật này . Bạn sẽ che giấu một điều kiện cuộc đua - sẽ tốt hơn để tiết lộ nó! Ngoại lệ null đó giúp phát hiện sự lạm dụng thành phần của bạn. Nếu bạn muốn thành phần của mình được bảo vệ khỏi lạm dụng, bạn có thể làm theo ví dụ của WPF - lưu ID luồng trong hàm tạo của bạn và sau đó ném ngoại lệ nếu một luồng khác cố gắng tương tác trực tiếp với thành phần của bạn. Hoặc khác thực hiện một thành phần thực sự an toàn chủ đề (không phải là một nhiệm vụ dễ dàng).

Vì vậy, tôi cho rằng chỉ đơn thuần thực hiện bản sao / kiểm tra thành ngữ này là lập trình sùng bái hàng hóa, thêm lộn xộn và tiếng ồn vào mã của bạn. Để thực sự bảo vệ chống lại các chủ đề khác đòi hỏi nhiều công việc hơn.

Cập nhật để phản hồi các bài đăng trên blog của Eric Lippert:

Vì vậy, có một điều quan trọng mà tôi đã bỏ lỡ về các trình xử lý sự kiện: "các trình xử lý sự kiện được yêu cầu phải mạnh mẽ khi được gọi ngay cả sau khi sự kiện đã bị hủy đăng ký", và do đó chúng ta chỉ cần quan tâm đến khả năng của sự kiện đại biểu được null. Là yêu cầu trên xử lý sự kiện tài liệu bất cứ nơi nào?

Và vì vậy: "Có nhiều cách khác để giải quyết vấn đề này, ví dụ: khởi tạo trình xử lý để có một hành động trống không bao giờ bị xóa. Nhưng thực hiện kiểm tra null là mẫu chuẩn."

Vì vậy, phần còn lại của câu hỏi của tôi là, tại sao rõ ràng-null-kiểm tra "mẫu chuẩn"? Giải pháp thay thế, chỉ định đại biểu trống, chỉ yêu cầu = delegate {}được thêm vào tuyên bố sự kiện và điều này giúp loại bỏ những đống lễ nghi hôi thối từ mọi nơi diễn ra sự kiện. Sẽ thật dễ dàng để đảm bảo rằng đại biểu trống có giá rẻ để khởi tạo. Hay tôi vẫn còn thiếu một cái gì đó?

Chắc chắn đó phải là (như Jon Skeet đã đề xuất) đây chỉ là lời khuyên .NET 1.x chưa chết, như nó nên được thực hiện vào năm 2005?


4
Câu hỏi này xuất hiện trong một cuộc thảo luận nội bộ một thời gian trước; Tôi đã có ý định viết blog này một thời gian rồi. Bài đăng của tôi về chủ đề này là ở đây: Sự kiện và chủng tộc
Eric Lippert

3
Stephen Cleary có một bài viết CodeProject kiểm tra vấn đề này và ông kết luận một mục đích chung, giải pháp "an toàn luồng" không tồn tại. Về cơ bản, tùy thuộc vào người gọi sự kiện để đảm bảo rằng đại biểu không phải là null và người xử lý sự kiện có thể xử lý việc được gọi sau khi hủy đăng ký.
rkagerer

3
@rkagerer - thực sự vấn đề thứ hai đôi khi phải được xử lý bởi trình xử lý sự kiện ngay cả khi các chủ đề không liên quan. Có thể xảy ra nếu một trình xử lý sự kiện yêu cầu một trình xử lý khác hủy đăng ký khỏi sự kiện hiện đang được xử lý, nhưng người đăng ký thứ 2 sau đó sẽ nhận được sự kiện (vì nó đã bị hủy trong khi quá trình xử lý đang diễn ra).
Daniel Earwicker

3
Thêm đăng ký vào một sự kiện có số người đăng ký bằng 0, xóa đăng ký duy nhất cho sự kiện, gọi một sự kiện có số người đăng ký bằng 0 và gọi một sự kiện với chính xác một người đăng ký, tất cả đều hoạt động nhanh hơn nhiều so với việc thêm / xóa / gọi các tình huống liên quan đến các số khác của thuê bao. Thêm một đại biểu giả làm chậm trường hợp phổ biến. Vấn đề thực sự với C # là người tạo ra nó đã quyết định EventName(arguments)gọi đại biểu của sự kiện một cách vô điều kiện, thay vì chỉ gọi đại biểu nếu không null (không làm gì nếu null).
supercat

Câu trả lời:


100

JIT không được phép thực hiện tối ưu hóa mà bạn đang nói đến trong phần đầu tiên, vì điều kiện. Tôi biết điều này đã được nêu ra như một bóng ma một thời gian trước đây, nhưng nó không hợp lệ. (Tôi đã kiểm tra nó với Joe Duffy hoặc Vance Morrison một lúc trước; tôi không thể nhớ cái nào.)

Nếu không có công cụ sửa đổi dễ bay hơi, có thể bản sao cục bộ đã lấy sẽ hết hạn, nhưng đó là tất cả. Nó sẽ không gây ra a NullReferenceException.

Và vâng, chắc chắn có một điều kiện cuộc đua - nhưng sẽ luôn luôn có. Giả sử chúng ta chỉ thay đổi mã thành:

TheEvent(this, EventArgs.Empty);

Bây giờ giả sử rằng danh sách gọi cho đại biểu đó có 1000 mục. Hoàn toàn có khả năng rằng hành động ở đầu danh sách sẽ được thực hiện trước khi một luồng khác hủy đăng ký một trình xử lý ở gần cuối danh sách. Tuy nhiên, trình xử lý đó vẫn sẽ được thực thi vì nó sẽ là một danh sách mới. (Đại biểu là bất biến.) Theo như tôi có thể thấy điều này là không thể tránh khỏi.

Sử dụng một đại biểu trống chắc chắn tránh được kiểm tra vô hiệu, nhưng không khắc phục điều kiện cuộc đua. Nó cũng không đảm bảo rằng bạn luôn "thấy" giá trị mới nhất của biến.


4
"Lập trình đồng thời trên Windows" của Joe Duffy bao gồm khía cạnh mô hình bộ nhớ và tối ưu hóa JIT của câu hỏi; xem mã.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger

2
Tôi đã chấp nhận điều này dựa trên nhận xét về lời khuyên "tiêu chuẩn" là tiền C # 2 và tôi không nghe thấy ai mâu thuẫn với điều đó. Trừ khi việc lập trình sự kiện của bạn là rất tốn kém, chỉ cần đặt '= ủy nhiệm {}' vào cuối tuyên bố sự kiện của bạn và sau đó gọi trực tiếp các sự kiện của bạn như thể chúng là phương thức; không bao giờ gán null cho họ. (Các nội dung khác mà tôi đã đưa ra về việc đảm bảo trình xử lý không được gọi sau khi hủy bỏ, đó là tất cả không liên quan và không thể đảm bảo, ngay cả đối với mã đơn, ví dụ: nếu trình xử lý 1 yêu cầu trình xử lý 2 hủy bỏ, trình xử lý 2 vẫn sẽ được gọi tiếp theo.)
Daniel Earwicker

2
Trường hợp vấn đề duy nhất (như mọi khi) là các cấu trúc, trong đó bạn không thể đảm bảo rằng chúng sẽ được khởi tạo với bất kỳ thứ gì ngoài giá trị null trong các thành viên của chúng. Nhưng mưu đồ hút.
Daniel Earwicker

1
Về đại biểu trống, xem thêm câu hỏi này: stackoverflow.com/questions/170907/ cường .
Vladimir

2
@Tony: Về cơ bản vẫn còn một điều kiện chạy đua giữa một cái gì đó đăng ký / hủy đăng ký và đại biểu được thực thi. Mã của bạn (chỉ cần duyệt qua một thời gian ngắn) làm giảm tình trạng chủng tộc đó bằng cách cho phép đăng ký / hủy đăng ký có hiệu lực trong khi nó được nâng lên, nhưng tôi nghi ngờ rằng trong hầu hết các trường hợp hành vi bình thường không đủ tốt, điều này cũng không tốt.
Jon Skeet

52

Tôi thấy rất nhiều người hướng tới phương pháp mở rộng để làm điều này ...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

Điều đó cung cấp cho bạn cú pháp đẹp hơn để nâng cao sự kiện ...

MyEvent.Raise( this, new MyEventArgs() );

Và cũng không đi với bản sao cục bộ vì nó được chụp vào thời gian gọi phương thức.


9
Tôi thích cú pháp, nhưng hãy rõ ràng ... nó không giải quyết được vấn đề với trình xử lý cũ được gọi ngay cả khi nó chưa được đăng ký. Điều này chỉ giải quyết vấn đề vô hiệu hóa null. Trong khi tôi thích cú pháp, tôi đặt câu hỏi liệu nó có thực sự tốt hơn: sự kiện công khai EventHandler <T> MyEvent = xóa {}; ... MyEvent (này, MyEventArss ()) mới; Đây cũng là một giải pháp ma sát rất thấp mà tôi thích vì sự đơn giản của nó.
Simon Gillbee

@Simon Tôi thấy những người khác nhau nói những điều khác nhau về điều này. Tôi đã kiểm tra nó và những gì tôi đã làm cho tôi biết rằng điều này xử lý vấn đề xử lý null. Ngay cả khi Chìm ban đầu hủy đăng ký khỏi Sự kiện sau khi xử lý! = Null, sự kiện vẫn được nêu ra và không có ngoại lệ nào được ném.
JP Alioto


1
+1. Tôi chỉ tự viết phương pháp này, bắt đầu nghĩ về an toàn luồng, đã thực hiện một số nghiên cứu và vấp phải câu hỏi này.
Niels van der Rest

Làm thế nào điều này có thể được gọi từ VB.NET? Hay 'RaiseEvent' đã phục vụ cho các tình huống đa luồng?

35

"Tại sao rõ ràng-null-kiểm tra 'mẫu chuẩn'?"

Tôi nghi ngờ lý do cho điều này có thể là vì kiểm tra null có hiệu suất cao hơn.

Nếu bạn luôn đăng ký một đại biểu trống cho các sự kiện của bạn khi chúng được tạo, sẽ có một số chi phí:

  • Chi phí xây dựng đại biểu trống.
  • Chi phí xây dựng một chuỗi đại biểu để chứa nó.
  • Chi phí gọi đại biểu vô nghĩa mỗi khi sự kiện được nêu ra.

(Lưu ý rằng các điều khiển UI thường có số lượng lớn các sự kiện, hầu hết trong số đó không bao giờ được đăng ký. Phải tạo một thuê bao giả cho mỗi sự kiện và sau đó gọi nó có thể sẽ là một hiệu suất đáng kể.)

Tôi đã thực hiện một số thử nghiệm hiệu năng đáng chú ý để thấy tác động của phương pháp đại biểu đăng ký trống, và đây là kết quả của tôi:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

Lưu ý rằng đối với trường hợp không có hoặc một người đăng ký (phổ biến cho các điều khiển UI, nơi các sự kiện rất phong phú), sự kiện được khởi tạo trước với một đại biểu trống sẽ chậm hơn đáng kể (hơn 50 triệu lần lặp ...)

Để biết thêm thông tin và mã nguồn, hãy truy cập bài đăng trên blog này về .NET Sự an toàn của chủ đề gọi sự kiện mà tôi đã xuất bản chỉ một ngày trước khi câu hỏi này được hỏi (!)

(Thiết lập thử nghiệm của tôi có thể thiếu sót, vì vậy hãy tải xuống mã nguồn và tự kiểm tra nó. Mọi phản hồi đều được đánh giá cao.)


8
Tôi nghĩ rằng bạn thực hiện điểm quan trọng trong bài đăng trên blog của bạn: không cần phải lo lắng về ý nghĩa hiệu suất cho đến khi nó là một nút cổ chai. Tại sao để cách xấu xí là cách được đề nghị? Nếu chúng tôi muốn tối ưu hóa sớm thay vì rõ ràng, chúng tôi sẽ sử dụng trình biên dịch chương trình - vì vậy câu hỏi của tôi vẫn còn và tôi nghĩ câu trả lời có thể là lời khuyên chỉ đơn giản là trước các đại biểu ẩn danh và phải mất một thời gian dài để văn hóa con người thay đổi lời khuyên cũ, như trong "câu chuyện nồi rang" nổi tiếng.
Daniel Earwicker

13
Và số liệu của bạn chứng minh điểm rất rõ: tổng chi phí giảm xuống chỉ còn hai NANOSECONDS (!!!) cho mỗi sự kiện được nêu ra (pre-init so với classic-null). Điều này sẽ không thể phát hiện được trong hầu hết các ứng dụng với công việc thực tế, nhưng do phần lớn việc sử dụng sự kiện nằm trong khung GUI, bạn phải so sánh điều này với chi phí sơn lại các phần của màn hình trong Winforms, v.v. nó thậm chí còn trở nên vô hình hơn trong phạm vi hoạt động của CPU thực và chờ đợi tài nguyên. Dù sao, bạn nhận được +1 từ tôi cho công việc khó khăn. :)
Daniel Earwicker

1
@DanielEarwicker đã nói đúng, bạn đã chuyển tôi trở thành người tin tưởng vào sự kiện công cộng WrapperDoneHandler OnWrapperDone = (x, y) => {}; mô hình.
Mickey Perlstein

1
Cũng có thể tốt khi đặt một Delegate.Combine/ Delegate.Removecặp trong trường hợp sự kiện có 0, một hoặc hai người đăng ký; nếu liên tục thêm và xóa cùng một thể hiện ủy nhiệm, sự khác biệt về chi phí giữa các trường hợp sẽ được đặc biệt rõ ràng vì Combinecó hành vi trong trường hợp đặc biệt nhanh khi một trong các đối số là null(chỉ trả về đối số khác) và Removerất nhanh khi hai đối số là bằng (chỉ trả về null).
supercat

12

Tôi thật sự rất thích đọc này - không phải! Mặc dù tôi cần nó để hoạt động với tính năng C # được gọi là sự kiện!

Tại sao không sửa lỗi này trong trình biên dịch? Tôi biết có những người MS đã đọc những bài đăng này, vì vậy xin đừng đốt nó!

1 - vấn đề Null ) Tại sao không biến sự kiện thành .Empty thay vì null ở vị trí đầu tiên? Có bao nhiêu dòng mã sẽ được lưu để kiểm tra null hoặc phải dán = delegate {}lên tờ khai? Hãy để trình biên dịch xử lý trường hợp rỗng, IE không làm gì cả! Nếu tất cả đều quan trọng với người tạo ra sự kiện, họ có thể kiểm tra .Empty và làm bất cứ điều gì họ quan tâm với nó! Nếu không, tất cả các kiểm tra null / đại biểu thêm là hack xung quanh vấn đề!

Thành thật mà nói, tôi mệt mỏi khi phải làm điều này với mọi sự kiện - hay còn gọi là mã soạn sẵn!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2 - vấn đề về điều kiện cuộc đua ) Tôi đã đọc bài đăng trên blog của Eric, tôi đồng ý rằng H (người xử lý) nên xử lý khi nó tự hủy bỏ, nhưng sự kiện có thể được thực hiện bất biến / an toàn không? IE, thiết lập một cờ khóa trên sáng tạo của nó, để bất cứ khi nào nó được gọi, nó sẽ khóa tất cả các đăng ký và hủy đăng ký nó trong khi thực thi?

Kết luận ,

Không phải ngôn ngữ hiện đại ngày nay được cho là để giải quyết những vấn đề như thế này cho chúng ta sao?


Đồng ý, cần có sự hỗ trợ tốt hơn cho việc này trong trình biên dịch. Cho đến lúc đó, tôi đã tạo ra một khía cạnh PostSharp thực hiện điều này trong một bước hậu biên dịch . :)
Steven Jeuris

4
Khối yêu cầu đăng ký / hủy đăng ký luồng trong khi chờ mã bên ngoài tùy ý hoàn thành tồi tệ hơn nhiều so với việc người đăng ký nhận sự kiện sau khi đăng ký bị hủy, đặc biệt là "vấn đề" sau có thể được giải quyết dễ dàng bằng cách đơn giản là người xử lý sự kiện kiểm tra cờ để xem họ vẫn quan tâm đến việc nhận sự kiện của họ, nhưng những bế tắc do thiết kế cũ có thể gây ra.
supercat

@siêu mèo. Imo, bình luận "tệ hơn nhiều" là phụ thuộc vào ứng dụng. Ai sẽ không muốn khóa rất nghiêm ngặt mà không có cờ bổ sung khi đó là một tùy chọn? Bế tắc chỉ xảy ra nếu luồng xử lý sự kiện đang chờ trên một luồng khác (đó là đăng ký / hủy đăng ký) vì các khóa được đăng ký lại cùng luồng và đăng ký / hủy đăng ký trong trình xử lý sự kiện ban đầu sẽ không bị chặn. Nếu có chuỗi chéo đang chờ như một phần của trình xử lý sự kiện sẽ là một phần của thiết kế tôi muốn làm lại. Tôi đến từ một góc ứng dụng phía máy chủ có các mẫu có thể dự đoán được.
crokusek

2
@crokusek: Việc phân tích cần thiết để chứng minh rằng một hệ thống không có bế tắc là dễ dàng nếu không có chu kỳ trong biểu đồ được định hướng kết nối mỗi khóa với tất cả các khóa thể cần thiết trong khi việc giữ [việc thiếu chu kỳ chứng minh hệ thống bế tắc miễn phí]. Cho phép mã tùy ý được gọi trong khi khóa được giữ sẽ tạo ra một cạnh trong biểu đồ "có thể cần thiết" từ khóa đó đến bất kỳ khóa nào mà mã tùy ý có thể có được (không hoàn toàn là mọi khóa trong hệ thống, nhưng không xa nó ). Sự tồn tại của các chu kỳ sẽ không ngụ ý rằng bế tắc sẽ xảy ra, nhưng ...
supercat

1
... sẽ làm tăng đáng kể mức độ phân tích cần thiết để chứng minh rằng nó không thể.
supercat

8

Với C # 6 trở lên, mã có thể được đơn giản hóa bằng ?.toán tử mới như trong:

TheEvent?.Invoke(this, EventArgs.Empty);

Đây là tài liệu MSDN.


5

Theo Jeffrey Richter trong cuốn sách CLR qua C # , phương pháp đúng là:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

Bởi vì nó buộc một bản sao tham khảo. Để biết thêm thông tin, xem phần Sự kiện của anh ấy trong cuốn sách.


Có thể tôi đang thiếu thứ, nhưng Interlocked.CompareExchange ném NullReferenceException nếu đối số đầu tiên của nó là null, đó chính xác là những gì chúng tôi muốn tránh. msdn.microsoft.com/en-us/l
Library / bb297966.aspx

1
Interlocked.CompareExchangesẽ thất bại nếu bằng cách nào đó được chuyển thành null ref, nhưng đó không giống với việc được chuyển refđến một vị trí lưu trữ (ví dụ NewMail) tồn tại và ban đầu giữ tham chiếu null.
supercat

4

Tôi đã sử dụng mẫu thiết kế này để đảm bảo rằng các trình xử lý sự kiện không được thực thi sau khi chúng bị hủy đăng ký. Nó hoạt động khá tốt cho đến nay, mặc dù tôi chưa thử bất kỳ cấu hình hiệu suất nào.

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

Tôi hầu như làm việc với Mono cho Android những ngày này và Android dường như không thích nó khi bạn cố cập nhật Chế độ xem sau khi Hoạt động của nó được gửi đến nền.


Trên thực tế, tôi thấy một người khác đang sử dụng một mô hình rất giống nhau ở đây: stackoverflow.com/questions/3668953/iêu
Ash

2

Thực tiễn này không phải là để thực thi một trật tự hoạt động nhất định. Đó thực sự là về việc tránh một ngoại lệ tham chiếu null.

Lý do đằng sau những người quan tâm đến ngoại lệ tham chiếu null và không phải điều kiện chủng tộc sẽ đòi hỏi một số nghiên cứu tâm lý sâu sắc. Tôi nghĩ nó có liên quan đến thực tế là việc khắc phục vấn đề tham chiếu null dễ dàng hơn nhiều. Khi đã được sửa, họ treo một biểu ngữ "Hoàn thành nhiệm vụ" lớn trên mã của họ và giải nén bộ đồ bay của họ.

Lưu ý: việc sửa điều kiện cuộc đua có thể liên quan đến việc sử dụng theo dõi cờ đồng bộ xem trình xử lý có nên chạy không


Tôi không yêu cầu một giải pháp cho vấn đề đó. Tôi tự hỏi tại sao có lời khuyên rộng rãi để phun thêm mã lộn xộn xung quanh việc bắn sự kiện, khi nó chỉ tránh một ngoại lệ null khi có một điều kiện chủng tộc khó phát hiện, vẫn sẽ tồn tại.
Daniel Earwicker

1
Vâng đó là quan điểm của tôi. Họ không quan tâm đến điều kiện cuộc đua. Họ chỉ quan tâm đến ngoại lệ tham chiếu null. Tôi sẽ chỉnh sửa nó thành câu trả lời của tôi.
dss539

Và quan điểm của tôi là: tại sao nó có ý nghĩa để quan tâm đến ngoại lệ tham chiếu null và không quan tâm đến điều kiện cuộc đua?
Daniel Earwicker

4
Một trình xử lý sự kiện được viết đúng nên được chuẩn bị để xử lý thực tế là bất kỳ yêu cầu cụ thể nào để đưa ra một sự kiện có xử lý có thể chồng lấp yêu cầu thêm hoặc xóa nó, có thể hoặc không thể kết thúc sự kiện được thêm hoặc xóa. Lý do các lập trình viên không quan tâm đến điều kiện cuộc đua là vì trong mã được viết đúng , việc ai thắng sẽ không thành vấn đề .
supercat

2
@ dss539: Mặc dù người ta có thể thiết kế một khung sự kiện sẽ chặn các yêu cầu hủy đăng ký cho đến khi các yêu cầu sự kiện đang chờ xử lý kết thúc, một thiết kế như vậy sẽ không thể cho bất kỳ sự kiện nào (ngay cả một Unloadsự kiện như sự kiện) để hủy đăng ký của đối tượng một cách an toàn cho các sự kiện khác. Bẩn thỉu. Tốt hơn là chỉ đơn giản nói rằng các yêu cầu hủy đăng ký sự kiện sẽ khiến các sự kiện bị hủy đăng ký "cuối cùng" và người đăng ký sự kiện nên kiểm tra khi chúng được gọi xem chúng có hữu ích gì không.
supercat

1

Vì vậy, tôi đến bữa tiệc muộn một chút. :)

Đối với việc sử dụng null thay vì mẫu đối tượng null để thể hiện các sự kiện không có người đăng ký, hãy xem xét kịch bản này. Bạn cần phải gọi một sự kiện, nhưng việc xây dựng đối tượng (EventArss) là không tầm thường và trong trường hợp phổ biến, sự kiện của bạn không có người đăng ký. Sẽ có lợi cho bạn nếu bạn có thể tối ưu hóa mã của mình để kiểm tra xem bạn có bất kỳ người đăng ký nào không trước khi bạn cam kết xử lý để xây dựng các đối số và gọi sự kiện.

Với suy nghĩ này, một giải pháp là nói "tốt, không có người đăng ký nào được đại diện bởi null." Sau đó, chỉ cần thực hiện kiểm tra null trước khi thực hiện thao tác đắt tiền của bạn. Tôi cho rằng một cách khác để làm điều này là có một thuộc tính Count trên loại Delegate, vì vậy bạn chỉ thực hiện thao tác đắt tiền nếu myDelegate.Count> 0. Sử dụng thuộc tính Count là một mô hình khá hay để giải quyết vấn đề ban đầu cho phép tối ưu hóa, và nó cũng có đặc tính tốt là có thể được gọi mà không gây ra NullReferenceException.

Tuy nhiên, hãy nhớ rằng vì các đại biểu là loại tham chiếu, nên chúng được phép là null. Có lẽ đơn giản là không có cách nào tốt để che giấu sự thật này dưới vỏ bọc và chỉ hỗ trợ mẫu đối tượng null cho các sự kiện, vì vậy phương án thay thế có thể đã buộc các nhà phát triển kiểm tra cả null và cho thuê bao không. Điều đó thậm chí còn xấu hơn tình hình hiện tại.

Lưu ý: Đây là đầu cơ thuần túy. Tôi không liên quan đến các ngôn ngữ .NET hoặc CLR.


Tôi giả sử bạn có nghĩa là "việc sử dụng đại biểu trống thay vì ..." Bạn đã có thể thực hiện những gì bạn đề xuất, với một sự kiện được khởi tạo cho đại biểu trống. Thử nghiệm (MyEvent.GetInvocationList (). Độ dài == 1) sẽ đúng nếu đại biểu trống ban đầu là thứ duy nhất trong danh sách. Vẫn sẽ không cần phải tạo một bản sao đầu tiên. Mặc dù tôi nghĩ rằng trường hợp bạn mô tả sẽ cực kỳ hiếm.
Daniel Earwicker

Tôi nghĩ rằng chúng tôi đang đưa ra ý tưởng của các đại biểu và sự kiện ở đây. Nếu tôi có một sự kiện Foo trên lớp của mình, thì khi người dùng bên ngoài gọi MyType.Foo + = / - =, họ thực sự đang gọi các phương thức add_Foo () và remove_Foo (). Tuy nhiên, khi tôi tham chiếu Foo từ trong lớp được xác định, tôi thực sự đang tham chiếu trực tiếp đại biểu bên dưới, chứ không phải các phương thức add_Foo () và remove_Foo (). Và với sự tồn tại của các loại như EventHandlerList, không có gì bắt buộc rằng đại biểu và sự kiện thậm chí ở cùng một nơi. Đây là những gì tôi muốn nói đến đoạn "Hãy ghi nhớ" trong phản hồi của tôi.
Levi

(tt.) Tôi thừa nhận rằng đây là một thiết kế khó hiểu, nhưng sự thay thế có thể tồi tệ hơn. Vì cuối cùng tất cả những gì bạn có là một đại biểu - bạn có thể tham khảo trực tiếp đại biểu cơ bản, bạn có thể lấy nó từ một bộ sưu tập, bạn có thể khởi tạo nó một cách nhanh chóng - về mặt kỹ thuật có thể không hỗ trợ bất cứ điều gì ngoài "kiểm tra null "mẫu.
Levi

Khi chúng ta đang nói về việc khai mạc sự kiện, tôi không thể hiểu tại sao các trình truy cập thêm / xóa lại quan trọng ở đây.
Daniel Earwicker

@Levi: Tôi thực sự không thích cách C # xử lý các sự kiện. Nếu tôi có máy khoan của mình, đại biểu sẽ được đặt một tên khác với sự kiện này. Từ bên ngoài một lớp, các hoạt động được phép duy nhất trên một tên sự kiện sẽ là +=-=. Trong lớp, các hoạt động được phép cũng sẽ bao gồm việc gọi (với kiểm tra null tích hợp), kiểm tra lại nullhoặc cài đặt thành null. Đối với bất cứ điều gì khác, người ta sẽ phải sử dụng đại biểu có tên sẽ là tên sự kiện với một số tiền tố hoặc hậu tố cụ thể.
supercat

0

đối với các applicaitons đơn, bạn không đúng, đây không phải là vấn đề.

Tuy nhiên, nếu bạn đang tạo một thành phần phơi bày các sự kiện, không có gì đảm bảo rằng người tiêu dùng thành phần của bạn sẽ không chuyển sang đa luồng, trong trường hợp đó bạn cần chuẩn bị cho điều tồi tệ nhất.

Sử dụng đại biểu trống không giải quyết được vấn đề, nhưng cũng gây ra hiệu ứng đánh vào mọi cuộc gọi đến sự kiện và có thể có ý nghĩa về GC.

Bạn đúng rằng người tiêu dùng đã hủy đăng ký để điều này xảy ra, nhưng nếu họ đã vượt qua bản sao tạm thời, thì hãy xem xét tin nhắn đã được chuyển.

Nếu bạn không sử dụng biến tạm thời và không sử dụng đại biểu trống và ai đó hủy đăng ký, bạn sẽ nhận được một ngoại lệ tham chiếu null, gây tử vong, vì vậy tôi nghĩ rằng chi phí đó là xứng đáng.


0

Tôi chưa bao giờ thực sự coi đây là một vấn đề vì tôi thường chỉ bảo vệ chống lại loại xấu này trong các phương thức tĩnh (vv) trên các thành phần có thể sử dụng lại của mình và tôi không tạo ra các sự kiện tĩnh.

Tôi đang làm sai à?


Nếu bạn phân bổ một thể hiện của một lớp với trạng thái có thể thay đổi (các trường thay đổi giá trị của chúng) và sau đó cho phép một số luồng truy cập cùng một thể hiện mà không sử dụng khóa để bảo vệ các trường đó khỏi bị sửa đổi bởi hai luồng cùng một lúc, bạn có lẽ làm sai Nếu tất cả các luồng của bạn có các trường hợp riêng (không chia sẻ gì) hoặc tất cả các đối tượng của bạn là bất biến (một khi được phân bổ, giá trị của các trường của chúng không bao giờ thay đổi) thì có lẽ bạn vẫn ổn.
Daniel Earwicker

Cách tiếp cận chung của tôi là để đồng bộ hóa cho người gọi ngoại trừ trong các phương thức tĩnh. Nếu tôi là người gọi, thì tôi sẽ đồng bộ hóa ở cấp cao hơn đó. (tất nhiên ngoại trừ một đối tượng có mục đích duy nhất là xử lý truy cập được đồng bộ hóa, tất nhiên. :))
Greg D

@GregD phụ thuộc vào mức độ phức tạp của phương thức và dữ liệu sử dụng. nếu nó ảnh hưởng đến các thành viên nội bộ và bạn quyết định chạy trong trạng thái phân luồng / Nhiệm vụ, bạn sẽ bị tổn thương rất nhiều
Mickey Perlstein

0

Dây tất cả các sự kiện của bạn tại xây dựng và để chúng một mình. Thiết kế của lớp Delegate không thể xử lý chính xác bất kỳ cách sử dụng nào khác, như tôi sẽ giải thích trong đoạn cuối của bài này.

Trước hết, không có lý do nào để cố gắng chặn thông báo sự kiện khi người xử lý sự kiện của bạn phải đưa ra quyết định đồng bộ về việc có / cách phản hồi thông báo hay không .

Bất cứ điều gì có thể được thông báo, nên được thông báo. Nếu người xử lý sự kiện của bạn đang xử lý đúng các thông báo (nghĩa là họ có quyền truy cập vào trạng thái ứng dụng có thẩm quyền và chỉ trả lời khi thích hợp), thì sẽ ổn khi thông báo cho họ bất cứ lúc nào và tin rằng họ sẽ phản hồi đúng.

Lần duy nhất một người xử lý không nên được thông báo rằng một sự kiện đã xảy ra, là nếu sự kiện trên thực tế đã không xảy ra! Vì vậy, nếu bạn không muốn một trình xử lý được thông báo, hãy ngừng tạo các sự kiện (nghĩa là vô hiệu hóa điều khiển hoặc bất cứ điều gì chịu trách nhiệm phát hiện và đưa sự kiện vào vị trí đầu tiên).

Thành thật mà nói, tôi nghĩ rằng lớp Delegate là không thể thay đổi. Việc sáp nhập / chuyển đổi sang MulticastDelegate là một sai lầm lớn, bởi vì nó đã thay đổi hiệu quả định nghĩa (hữu ích) của một sự kiện từ một điều gì đó xảy ra tại một thời điểm, sang một điều gì đó xảy ra trong một khoảng thời gian. Thay đổi như vậy đòi hỏi một cơ chế đồng bộ hóa có thể thu gọn nó một cách hợp lý thành một khoảnh khắc duy nhất, nhưng MulticastDelegate thiếu bất kỳ cơ chế nào như vậy. Đồng bộ hóa sẽ bao gồm toàn bộ thời gian hoặc ngay lập tức sự kiện diễn ra, để một khi ứng dụng đưa ra quyết định đồng bộ hóa để bắt đầu xử lý một sự kiện, nó sẽ hoàn thành xử lý hoàn toàn (giao dịch). Với hộp đen là lớp lai MulticastDelegate / Delegate, điều này gần như là không thể, vì vậytuân thủ việc sử dụng một thuê bao đơn và / hoặc triển khai loại MulticastDelegate của riêng bạn có tay cầm đồng bộ hóa có thể được gỡ bỏ trong khi chuỗi xử lý đang được sử dụng / sửa đổi . Tôi khuyên bạn nên làm điều này, bởi vì giải pháp thay thế sẽ là triển khai đồng bộ hóa / toàn vẹn giao dịch một cách dự phòng trong tất cả các trình xử lý của bạn, điều này sẽ vô cùng phức tạp / không cần thiết.


[1] Không có xử lý sự kiện hữu ích nào xảy ra tại "một khoảnh khắc tức thời". Tất cả các hoạt động có một khoảng thời gian. Bất kỳ trình xử lý đơn lẻ nào cũng có thể có một chuỗi các bước không tầm thường để thực hiện. Hỗ trợ một danh sách xử lý thay đổi không có gì.
Daniel Earwicker

[2] Giữ một khóa trong khi một sự kiện được bắn là sự điên rồ hoàn toàn. Nó chắc chắn dẫn đến bế tắc. Nguồn lấy ra khóa A, sự kiện cháy, bồn rửa lấy ra khóa B, bây giờ hai khóa được giữ. Điều gì xảy ra nếu một số hoạt động trong một luồng khác dẫn đến các khóa được lấy ra theo thứ tự ngược lại? Làm thế nào có thể loại trừ các kết hợp chết người như vậy khi trách nhiệm khóa được phân chia giữa các thành phần được thiết kế / thử nghiệm riêng biệt (đó là toàn bộ điểm của sự kiện)?
Daniel Earwicker

[3] Không có vấn đề nào trong số này làm giảm tính hữu dụng toàn diện của các đại biểu / sự kiện đa hướng thông thường trong thành phần đơn luồng của các thành phần, đặc biệt là trong khung GUI. Ca sử dụng này bao gồm phần lớn việc sử dụng các sự kiện. Sử dụng các sự kiện theo cách phân luồng tự do có giá trị đáng ngờ; điều này không làm mất hiệu lực thiết kế của họ hoặc tính hữu dụng rõ ràng của chúng trong bối cảnh nơi chúng có ý nghĩa.
Daniel Earwicker

[4] Chủ đề + sự kiện đồng bộ về cơ bản là cá trích đỏ. Giao tiếp không đồng bộ xếp hàng là cách để đi.
Daniel Earwicker

[1] Tôi không đề cập đến thời gian đo ... Tôi đã nói về các hoạt động nguyên tử, xảy ra một cách hợp lý ngay lập tức ... và điều đó có nghĩa là tôi không có gì khác liên quan đến cùng một tài nguyên mà họ đang sử dụng có thể thay đổi trong khi sự kiện đang diễn ra bởi vì nó được nối tiếp với một khóa.
Triynko

0

Vui lòng xem tại đây: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safe Đây là giải pháp chính xác và phải luôn được sử dụng thay vì tất cả các cách giải quyết khác.

Bạn có thể đảm bảo rằng danh sách gọi nội bộ luôn có ít nhất một thành viên bằng cách khởi tạo nó bằng phương thức ẩn danh không làm gì cả. Vì không có bên ngoài nào có thể có tham chiếu đến phương thức ẩn danh, nên không có bên ngoài nào có thể xóa phương thức đó, vì vậy đại biểu sẽ không bao giờ là null null - Lập trình .NET Các thành phần, Phiên bản 2, bởi Juval Löwy

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  

0

Tôi không tin rằng câu hỏi bị ràng buộc với loại "sự kiện" c #. Loại bỏ hạn chế đó, tại sao không phát minh lại bánh xe một chút và làm một cái gì đó dọc theo những dòng này?

Tăng chủ đề sự kiện một cách an toàn - thực hành tốt nhất

  • Khả năng phụ / hủy đăng ký từ bất kỳ chủ đề nào trong khi tăng (loại bỏ điều kiện cuộc đua)
  • Toán tử quá tải cho + = và - = ở cấp lớp.
  • Đại biểu xác định người gọi chung

0

Cảm ơn cho một cuộc thảo luận hữu ích. Tôi đã làm việc về vấn đề này gần đây và thực hiện lớp sau chậm hơn một chút, nhưng cho phép tránh các cuộc gọi đến các đối tượng bị loại bỏ.

Điểm chính ở đây là danh sách gọi có thể được sửa đổi ngay cả khi sự kiện được nêu ra.

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

Và cách sử dụng là:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

Kiểm tra

Tôi đã thử nó theo cách sau. Tôi có một chủ đề tạo và phá hủy các đối tượng như thế này:

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

Trong một hàm tạo Bar(một đối tượng người nghe) tôi đăng ký SomeEvent(được triển khai như được hiển thị ở trên) và hủy đăng ký trong Dispose:

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

Ngoài ra tôi có một vài chủ đề nâng sự kiện trong một vòng lặp.

Tất cả những hành động này được thực hiện đồng thời: nhiều người nghe được tạo ra và phá hủy và sự kiện đang được bắn cùng một lúc.

Nếu có một điều kiện cuộc đua, tôi sẽ thấy một thông báo trong bảng điều khiển, nhưng nó trống. Nhưng nếu tôi sử dụng các sự kiện clr như bình thường, tôi thấy nó chứa đầy các thông điệp cảnh báo. Vì vậy, tôi có thể kết luận rằng có thể thực hiện một sự kiện an toàn của luồng trong c #.

Bạn nghĩ sao?


Có vẻ đủ tốt với tôi. Mặc dù tôi nghĩ rằng có thể (về lý thuyết) có disposed = truethể xảy ra trước đây foo.SomeEvent -= Handlertrong ứng dụng thử nghiệm của bạn, tạo ra kết quả dương tính giả. Nhưng ngoài ra, có một vài điều bạn có thể muốn thay đổi. Bạn thực sự muốn sử dụng try ... finallycho các khóa - điều này sẽ giúp bạn làm cho nó không chỉ an toàn cho chủ đề, mà còn hủy bỏ an toàn. Chưa kể rằng bạn có thể thoát khỏi sự ngớ ngẩn đó try catch. Và bạn không kiểm tra đại biểu được thông qua Add/ Remove- có thể là null(bạn nên ném thẳng vào Add/ Remove).
Luaan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.