C # 이벤트 및 스레드 안전성


237

최신 정보

C # 6 부터이 질문에 대한 답변은 다음과 같습니다.

SomeEvent?.Invoke(this, e);

다음과 같은 조언을 자주 듣거나 읽습니다.

이벤트를 확인하고 시작하기 전에 항상 이벤트 사본을 만드 null십시오. 이렇게하면 nullnull을 확인하는 위치 와 이벤트 를 발생시키는 위치 사이에 이벤트가 발생하는 스레딩 관련 잠재적 문제가 제거됩니다 .

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

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

업데이트 : 이벤트 멤버가 일시적이어야 할 수 있다는 최적화에 대해 읽었지만 Jon Skeet은 CLR이 복사본을 최적화하지 않는다고 대답했습니다.

그러나이 문제가 발생하기 위해서는 다른 스레드가 다음과 같은 작업을 수행해야합니다.

// 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...

실제 순서는 다음과 같습니다.

// 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

OnTheEvent저자가 구독을 취소 한 후에 실행 되는 요점은 이러한 일이 발생하지 않도록 특별히 구독을 취소 한 것입니다. 실제로 필요한 것은 접근 자 addremove접근 자 에서 적절한 동기화를 통해 사용자 정의 이벤트 구현입니다 . 또한 이벤트가 발생하는 동안 잠금이 유지되면 교착 상태가 발생할 수 있습니다.

그래서이있다 화물 숭배 프로그래밍 ? 그런 식으로 보입니다-많은 사람들이 여러 스레드로부터 코드를 보호하기 위해이 단계를 수행해야합니다. 실제로 이벤트가 멀티 스레드 디자인의 일부로 사용되기 전에 이것보다 훨씬 더주의를 기울여야 할 것 같습니다 . 따라서 추가 관리를하지 않는 사람들은이 조언을 무시할 수도 있습니다. 단일 스레드 프로그램에서는 문제가되지 않으며 실제로 volatile대부분의 온라인 예제 코드가없는 경우에는 조언이 없을 수도 있습니다. 전혀 효과.

(그리고 delegate { }멤버 선언에 빈 것을 할당하는 것이 훨씬 간단하지 않으므로 null처음 부터 확인할 필요가 없습니다 .)

업데이트 :명확하지 않은 경우 모든 상황에서 null 참조 예외를 피하기 위해 조언의 의도를 파악했습니다. 내 요점은이 특정 null 참조 예외가 다른 스레드가 이벤트에서 delisting하는 경우에만 발생할 수 있으며 그 이유는 해당 기술을 통해 더 이상 호출이 수신되지 않도록하는 것입니다. . 경쟁 조건을 숨기고있을 것입니다. 공개하는 것이 좋습니다. 이 null 예외는 구성 요소의 남용을 감지하는 데 도움이됩니다. 구성 요소가 악용되지 않도록하려면 WPF의 예를 따르십시오. 스레드 ID를 생성자에 저장 한 다음 다른 스레드가 구성 요소와 직접 상호 작용하려고하면 예외를 throw 할 수 있습니다. 또는 진정한 스레드 안전 구성 요소를 구현하십시오 (쉬운 작업은 아님).

그래서 나는 단지이 카피 / 체크 관용구를하는 것은화물 컬트 프로그래밍이며, 코드에 혼란과 소음을 추가한다고 주장한다. 실제로 다른 스레드로부터 보호하려면 더 많은 작업이 필요합니다.

Eric Lippert의 블로그 게시물에 대한 답변으로 업데이트 :

따라서 이벤트 처리기에서 놓친 주요한 사항이 있습니다. "이벤트 처리기가 구독 취소 된 후에도 호출 될 때 이벤트 처리기가 강력해야합니다." 대리자 null. 이벤트 핸들러에 대한 요구 사항이 어디에나 문서화되어 있습니까?

"이 문제를 해결하는 다른 방법이 있습니다. 예를 들어, 처리기가 초기화되지 않은 빈 동작을 갖도록 초기화하는 것입니다. 그러나 null 검사를 수행하는 것이 표준 패턴입니다."

그래서 내 질문의 나머지 조각은 왜 명시 적 null 검사가 "표준 패턴"입니까? 빈 델리게이트를 할당하는 대안 = delegate {}은 이벤트 선언에 추가하기 만하면되며, 이렇게하면 이벤트가 발생하는 모든 장소에서 악의적 인 행사 더미가 제거됩니다. 빈 델리게이트가 인스턴스화하기에 저렴하다는 것을 쉽게 알 수 있습니다. 아니면 여전히 뭔가 빠졌습니까?

Jon Skeet이 제안한대로 2005 년에했던 것처럼 죽지 않은 .NET 1.x 조언 일 것입니다.


4
이 질문은 잠시 전에 내부 토론에서 나왔습니다. 나는 이것을 얼마 동안 블로그에 올릴 예정이었다. 주제에 관한 나의 포스트는 여기에있다 : 사건과 인종
Eric Lippert

3
Stephen Cleary는 이 문제를 검토 하는 CodeProject 기사 를 가지고 있으며 "스레드 안전"솔루션이 존재하지 않는다는 일반적인 목적을 결론지었습니다. 기본적으로 델리게이트가 null이 아닌지 확인하는 것은 이벤트 호출자에 달려 있으며, 구독이 취소 된 후에 호출되는 것을 처리 할 수있는 이벤트 핸들러에 달려 있습니다.
rkagerer

3
@rkagerer-스레드가 관련되지 않은 경우에도 실제로 두 번째 문제는 이벤트 핸들러에서 처리해야하는 경우가 있습니다. 하나의 이벤트 핸들러가 다른 핸들러에게 현재 처리중인 이벤트에서 구독을 취소하도록 지시 하지만, 두 번째 구독자가 어쨌든 이벤트를 수신합니다 (처리가 진행중인 동안 구독이 취소 되었기 때문에).
Daniel Earwicker

3
구독자가없는 이벤트에 구독을 추가하고, 이벤트에 대한 유일한 구독을 제거하고, 구독자가없는 이벤트를 호출하고, 정확히 하나의 구독자를 가진 이벤트를 호출하는 것은 다른 수의 다른 시나리오가있는 추가 / 제거 / 호출 시나리오보다 훨씬 빠른 조작입니다. 가입자. 더미 대리자를 추가하면 일반적인 경우 속도가 느려집니다. C #의 실제 문제는 생성자 EventName(arguments)가 널이 아닌 경우 델리게이트를 호출하지 않고 이벤트의 델리게이트를 무조건 호출 하기로 결정했다는 것입니다 (널이면 아무것도 수행하지 않음).
supercat

답변:


100

JIT는 조건 때문에 첫 번째 부분에서 이야기하는 최적화를 수행 할 수 없습니다. 나는 이것이 오래 전에 유령으로 제기되었다는 것을 알고 있지만 유효하지 않습니다. (나는 얼마 전에 Joe Duffy 또는 Vance Morrison으로 확인했지만 어느 것을 기억할 수 없습니다.)

휘발성 수정자가 없으면 로컬 복사본이 오래되었을 수 있지만 그게 전부입니다. 을 발생시키지 않습니다 NullReferenceException.

물론 그렇습니다. 경쟁 조건이 있지만 항상있을 것입니다. 코드를 다음과 같이 변경한다고 가정합니다.

TheEvent(this, EventArgs.Empty);

이제 해당 대리자의 호출 목록에 1000 개의 항목이 있다고 가정하십시오. 다른 스레드가 목록의 끝 근처에서 핸들러를 구독 취소하기 전에 목록 시작시 조치가 실행되었을 가능성이 있습니다. 그러나 해당 핸들러는 새 목록이므로 계속 실행됩니다. (대표는 불변이다.) 내가 볼 수있는 한, 이것은 불가피하다.

빈 델리게이트를 사용하면 nullity 검사를 피할 수 있지만 경쟁 조건을 수정하지는 않습니다. 또한 항상 변수의 최신 값을 "볼"것을 보장하지는 않습니다.


4
Joe Duffy의 "Windows에서의 동시 프로그래밍"은 문제의 JIT 최적화 및 메모리 모델 측면을 다룹니다. code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger

2
저는 C # 2 이전의 "표준"조언에 대한 의견을 바탕으로이를 수락했으며, 모순되는 사람의 말을 듣지 않습니다. 이벤트 인수를 인스턴스화하는 데 실제로 비용이 많이 들지 않는 한, 이벤트 선언 끝에 '= delegate {}'를 입력 한 다음 메소드처럼 이벤트를 직접 호출하십시오. 그들에게 null을 할당하지 마십시오. (내가 처리기를 delisting 한 후에 호출하지 않도록하는 데 가져온 다른 것들은 모두 관련이 없으며 단일 스레드 코드의 경우에도 처리가 불가능합니다. 다음).
다니엘 Earwicker

2
항상 그렇듯이 유일한 문제는 구조체이며, 멤버에서 null 값 이외의 다른 인스턴스로 인스턴스화 할 수는 없습니다. 그러나 구조체는 빨라요.
Daniel Earwicker

1
빈 위임에 대해,이 질문을 참조하십시오 stackoverflow.com/questions/170907/...를 .
Vladimir

2
@Tony : 구독 / 구독 취소와 실행중인 델리게이트간에 여전히 근본적으로 경쟁 조건이 있습니다. 코드 (간단히 탐색 한 코드)는 구독 / 구독 취소가 발생하는 동안 적용되도록 허용하여 경쟁 조건을 줄이지 만 대부분의 경우 정상적인 동작으로 충분하지 않은 것으로 생각됩니다.
Jon Skeet

52

나는 이것을하는 확장 방법을 향해 많은 사람들을 봅니다 ...

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

이벤트를 발생시키는 더 좋은 구문을 제공합니다 ...

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

또한 메소드 호출시 캡처되므로 로컬 사본을 제거합니다.


9
구문이 마음에 들지만 명확하게 해 봅시다 ... 등록이 취소 된 후에도 오래된 처리기가 호출되는 문제를 해결하지 못합니다. 이것은 null 역 참조 문제 해결합니다. 구문이 마음에 들지만 실제로 다음보다 나은지 질문합니다. public event EventHandler <T> MyEvent = delete {}; ... MyEvent (이것, 새로운 MyEventArgs ()); 이것은 또한 단순성으로 인해 매우 저 마찰 솔루션입니다.
Simon Gillbee

@Simon 다른 사람들이 이것에 대해 다른 말을하는 것을 봅니다. 나는 그것을 테스트했고 내가 한 것은 이것이 null 처리기 문제를 처리한다는 것을 나에게 나타낸다. 핸들러! = null 확인 후 원래 싱크가 이벤트에서 등록을 해제하더라도 이벤트는 계속 발생하며 예외는 발생하지 않습니다.
JP Alioto


1
+1. 방금 스레드 안전성에 대해 생각하기 시작 하여이 방법을 직접 작성하고 약간의 연구를 수행 하여이 질문에 부딪 쳤습니다.
Niels van der Rest 9

VB.NET에서 어떻게 호출 할 수 있습니까? 또는 'RaiseEvent'는 이미 멀티 스레딩 시나리오에 적합합니까?

35

"왜 '표준 패턴'을 명시 적으로 null 검사합니까?"

그 이유는 null-check가 더 성능이 좋기 때문일 수 있습니다.

이벤트가 생성 될 때 항상 빈 델리게이트를 구독하면 몇 가지 오버 헤드가 발생합니다.

  • 빈 델리게이트 구성 비용.
  • 이를 포함 할 델리게이트 체인 구성 비용.
  • 이벤트가 발생할 때마다 무의미한 대리인을 호출하는 비용.

(UI 컨트롤에는 이벤트가 많지만 대부분 구독하지 않습니다. 각 이벤트에 더미 구독자를 만든 다음 호출하면 성능이 크게 저하 될 수 있습니다.)

subscribe-empty-delegate 접근법의 영향을 확인하기 위해 약간의 성능 테스트를 수행했으며 결과는 다음과 같습니다.

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

가입자가 0 명 또는 1 명인 경우 (이벤트가 많은 UI 컨트롤에 공통), 빈 델리게이트로 사전 초기화 된 이벤트는 현저하게 느립니다 (5 천만 회 이상 반복).

자세한 내용과 소스 코드를 보려면 이 질문을하기 바로 전에 게시 한 .NET 이벤트 호출 스레드 안전성 에 대한이 블로그 게시물을 방문하십시오 (!).

(테스트 설정에 결함이있을 수 있으므로 소스 코드를 다운로드하여 직접 검사하십시오. 의견은 대단히 감사합니다.)


8
블로그 게시물의 핵심은 병목 현상이 될 때까지 성능에 대한 걱정이 필요 없다는 것입니다. 왜 못생긴 방법이 권장되는 방법이 되었습니까? 명확하지 않고 조기 최적화를 원한다면 어셈블러를 사용하고 있습니다. 따라서 질문은 여전히 ​​남아 있습니다. 그리고 그 조언은 단순히 익명의 대의원보다 조언이 앞선다는 것입니다. 유명한 "냄비 로스트 스토리"에서.
Daniel Earwicker

13
그리고 당신의 수치는 그 요점을 매우 잘 입증합니다 : 오버 헤드는 발생하는 이벤트 (초기 대 고전 널) 당 NANOSECONDS (!!!)로 단지 2.5 반으로 떨어집니다. 실제 작업을 수행하는 거의 모든 앱에서 감지 할 수 없지만 대부분의 이벤트 사용이 GUI 프레임 워크에서 발생한다는 것을 감안할 때 Winforms 등의 화면 일부를 다시 칠하는 비용과 비교해야합니다. 실제 CPU 작업이 급증하고 리소스를 기다리는 동안에는 더 보이지 않습니다. 어쨌든, 당신은 나로부터 +1을 얻습니다. :)
Daniel Earwicker

1
@DanielEarwicker가 말했듯이 공개 이벤트의 신자가되도록 나를 옮겼습니다. WrapperDoneHandler OnWrapperDone = (x, y) => {}; 모델.
미키 펄 스테인

1
이벤트에 가입자가 0 명, 1 명 또는 2 명인 경우 Delegate.Combine/ Delegate.Remove쌍의 시간을 정하는 것이 좋습니다 . 하나가 동일한 대리자 인스턴스를 반복적으로 추가하고 제거하는 Combine경우 인수 중 하나가 null(다른 하나만 반환하면) 빠른 특수 사례 동작이 있기 때문에 사례 간의 비용 차이가 특히 두드러 지며 Remove두 인수가 같음 (null 만 반환)
supercat

12

나는이 읽기를 정말로 즐겼습니다 – 아닙니다! events라는 C # 기능을 사용하려면 필요하지만!

컴파일러에서 이것을 고치지 않겠습니까? 이 게시물을 읽는 MS 사람들이 있다는 것을 알고 있으므로 이것을 화 내지 마십시오!

1-Null 문제 ) 왜 이벤트를 null 대신에 비워 두지 않겠습니까? null 검사를 위해 몇 줄의 코드를 저장하거나 = delegate {}선언 을 고수 해야합니까? 컴파일러가 Empty 케이스를 처리하도록하자. IE는 아무것도하지 않는다! 모든 것이 이벤트 제작자에게 중요한 경우, .Empty를 확인하고 관심있는 모든 작업을 수행 할 수 있습니다! 그렇지 않으면 모든 null 확인 / 대리자 추가가 문제를 해결하는 것입니다!

솔직히 나는 모든 이벤트-일명 상용구 코드 로이 작업을 수행하는 데 피곤합니다!

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-경쟁 조건 문제 ) Eric의 블로그 게시물을 읽었으며 H (핸들러)가 자체를 역 참조 할 때 처리해야하지만 이벤트를 변경할 수없는 스레드로 만들 수 없다는 데 동의합니다. IE, 생성시 잠금 플래그를 설정하여 호출 될 때마다 모든 구독 및 구독을 잠그는 동안 실행합니까?

결론 ,

현대 언어는 우리에게 이런 문제를 해결하지 않습니까?


합의하면 컴파일러에서 더 나은 지원이 있어야합니다. 그때까지 나는 포스트 컴파일 단계에서 이것을 수행하는 PostSharp aspect를 만들었습니다 . :)
Steven Jeuris

4
임의의 외부 코드가 완료 될 때 까지 기다리는 동안 스레드 구독 / 등록 해제 요청 차단을하는 것은 구독이 취소 된 후 구독자가 이벤트를 수신하는 것보다 훨씬 나쁩니다 . 특히 이벤트 핸들러가 플래그를 확인하여 플래그를 확인 하여 후자의 "문제"를 쉽게 해결할 수 있기 때문입니다. 그들은 여전히 ​​이벤트를받는 데 관심이 있지만, 이전 디자인으로 인한 교착 상태는 다루기 어려울 수 있습니다.
supercat

@ 슈퍼 캣. 이모, "훨씬 더 나쁜"의견은 응용 프로그램에 따라 다릅니다. 옵션이있을 때 추가 플래그없이 매우 엄격한 잠금을 원하지 않는 사람은 누구입니까? 잠금은 동일한 스레드 재진입이며 원래 이벤트 핸들러 내의 구독 / 구독이 차단되지 않기 때문에 이벤트 처리 스레드가 대기중인 또 다른 스레드 (구독 / 구독 취소) 인 경우에만 교착 상태가 발생해야합니다. 디자인의 일부가 될 이벤트 처리기의 일부로 크로스 스레드가 대기하는 경우 재 작업을 선호합니다. 예측 가능한 패턴이있는 서버 측 앱 각도에서 왔습니다.
crokusek

2
@crokusek : 각 잠금을 유지하는 동안 필요할 있는 모든 잠금에 연결하는 직접 그래프에주기가없는 경우 시스템에 교착 상태가 없음 을 증명 하는 데 필요한 분석 [사이클 부족으로 시스템이 입증 됨] 교착 상태 없음]. 잠금이 유지 된 상태에서 임의의 코드를 호출하면 해당 잠금에서 임의의 코드가 획득 할 수있는 잠금 (시스템의 모든 잠금은 아니지만 멀지 않은 잠금)까지 "필요한"그래프의 에지가 생성됩니다. ). 결과적으로 사이클의 존재는 교착 상태가 발생한다는 것을 의미하지는 않지만 ...
supercat

1
불가능하다는 것을 증명하는 데 필요한 분석 수준을 크게 높일 것입니다.
supercat

8

함께 C # 6 위, 새로운 코드는 사용을 간략화 할 수 ?.있는 바와 같이 운영자 :

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

MSDN 설명서는 다음과 같습니다 .


5

C #을 통한 CLR 책의 Jeffrey Richter에 따르면 올바른 방법은 다음과 같습니다.

// 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);

참조 사본을 강제 실행하기 때문입니다. 자세한 내용은이 책의 이벤트 섹션을 참조하십시오.


내가 누락 된 것일 수도 있지만 Interlocked.CompareExchange는 첫 번째 인수가 null 인 경우 NullReferenceException을 throw하여 정확히 피하고 싶습니다. msdn.microsoft.com/ko-kr/library/bb297966.aspx
Kniganapolke

1
Interlocked.CompareExchange그것이 어떻게 든 null을 전달하면 실패 ref하지만, 존재하고 초기 에 null 참조를 보유 하는 ref저장 위치 (예 :)로 전달되는 것과 동일하지 않습니다 . NewMail
supercat

4

이 디자인 패턴을 사용하여 이벤트 핸들러가 등록 해제 된 후에 실행되지 않도록했습니다. 성능 프로파일 링을 시도하지 않았지만 지금까지는 잘 작동하고 있습니다.

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

요즘에는 주로 Android 용 Mono로 작업하고 있으며 활동이 백그라운드로 전송 된 후 View를 업데이트하려고하면 Android가 좋아하지 않는 것 같습니다.


실제로, 나는 다른 누군가가 여기에서 매우 유사한 패턴을 사용하고있는 것을 본다 : stackoverflow.com/questions/3668953/…
Ash

2

이 관행은 특정 작업 순서를 시행하는 것이 아닙니다. 실제로 null 참조 예외를 피하는 것입니다.

경쟁 조건이 아닌 null 참조 예외에 관심을 가진 사람들의 배후에는 심리적 심층 연구가 필요합니다. null 참조 문제를 수정하는 것이 훨씬 쉽다는 사실과 관련이 있다고 생각합니다. 일단 수정되면 코드에 큰 "미션 달성"배너를 매달고 비 행복을 풉니 다.

참고 : 경쟁 조건을 수정하려면 핸들러 실행 여부를 동기 플래그 트랙을 사용해야합니다.


그 문제에 대한 해결책을 요구하지 않습니다. 감지하기 어려운 경쟁 조건이 여전히 존재할 때 null 예외를 피할 때 이벤트 발생과 관련하여 추가 코드 메시지를 뿌리는 이유가 궁금합니다.
Daniel Earwicker

1
그게 내 요점이었다. 그들은 경쟁 조건에 관심 이 없습니다 . 그들은 null 참조 예외에만 관심이 있습니다. 나는 그것을 내 대답으로 편집 할 것입니다.
dss539

그리고 내 요점은 : null 참조 예외에 대해서는 신경 쓰지만 경쟁 조건 에는 신경 쓰지 않는 이유는 무엇입니까?
Daniel Earwicker

4
처리를 추가 또는 제거하기위한 요청과 겹칠 수있는 이벤트를 발생시키기위한 특정 요청이 추가되거나 제거되는 이벤트를 발생시킬 수도 있고 그렇지 않을 수도 있다는 사실을 처리하기 위해 올바르게 작성된 이벤트 핸들러를 준비해야합니다. 프로그래머가 경쟁 조건에 신경 쓰지 않는 이유는 올바르게 작성된 코드에서 누가이기는지 중요하지 않기 때문입니다 .
supercat

2
@ dss539 : 보류중인 이벤트 호출이 완료 될 때까지 구독 취소 요청을 차단하는 이벤트 프레임 워크를 디자인 할 수 있지만 이러한 디자인은 이벤트 (이벤트와 같은 Unload이벤트)가 다른 이벤트에 대한 객체의 구독을 안전하게 취소하는 것을 불가능하게 합니다. 추잡한. 이벤트 구독 취소 요청으로 인해 이벤트가 "최종적으로"구독 취소되고, 이벤트 구독자는 호출 될 때 유용한 작업이 있는지 확인해야합니다.
supercat

1

그래서 나는 파티에 조금 늦었다. :)

구독자가없는 이벤트를 표시하기 위해 널 오브젝트 패턴 대신 널을 사용하는 경우이 시나리오를 고려하십시오. 이벤트를 호출해야하지만 객체 (EventArgs) 구성은 쉽지 않으며 일반적인 경우 이벤트에 가입자가 없습니다. 인수를 구성하고 이벤트를 호출하기 위해 처리 노력을 커밋하기 전에 가입자가 있는지 확인하기 위해 코드를 최적화 할 수 있다면 도움이 될 것입니다.

이를 염두에두고 해결책은 "제로 가입자 수가 0으로 표시된다"는 것입니다. 그런 다음 값 비싼 작업을 수행하기 전에 null 검사를 수행하면됩니다. 이 작업을 수행하는 또 다른 방법은 Delegate 유형에 Count 속성을 사용하는 것이 었으므로 myDelegate.Count> 0 인 경우 비싼 작업 만 수행합니다. Count 속성을 사용하면 원래 문제를 해결하는 다소 좋은 패턴입니다. 최적화를 허용하고 NullReferenceException을 발생시키지 않고 호출 할 수있는 멋진 속성을 가지고 있습니다.

그러나 델리게이트는 참조 유형이므로 null이 될 수 있습니다. 아마도이 사실을 숨기고 이벤트에 대해 널 오브젝트 패턴 만 지원하는 좋은 방법이 없었을 것이므로, 대안으로 개발자가 널 (null) 및 제로 (subscriber) 가입자를 모두 확인하도록 강제했을 수 있습니다. 그것은 현재 상황보다 더 나빠질 것입니다.

참고 : 이것은 순수한 추측입니다. .NET 언어 또는 CLR과 관련이 없습니다.


나는 당신이 "빈 델리게이트 대신에 빈 델리게이트를 사용하는 것"을 의미한다고 가정한다. 당신은 이미 빈 델리게이트로 초기화 된 이벤트로 당신이 제안한 것을 할 수있다. 초기 비어있는 대리자가 목록에있는 유일한 경우 테스트 (MyEvent.GetInvocationList (). Length == 1)가 적용됩니다. 여전히 사본을 먼저 만들 필요는 없습니다. 당신이 묘사하는 사건은 어쨌든 극히 드물다고 생각합니다.
Daniel Earwicker

우리는 여기에서 대의원과 이벤트의 아이디어를 접목하고 있다고 생각합니다. 클래스에 Foo 이벤트가 있으면 외부 사용자가 MyType.Foo + = /-=를 호출하면 실제로 add_Foo () 및 remove_Foo () 메서드를 호출합니다. 그러나 Foo가 정의 된 클래스 내에서 Foo를 참조하면 실제로 add_Foo () 및 remove_Foo () 메서드가 아닌 기본 대리자를 직접 참조합니다. 그리고 EventHandlerList와 같은 유형이 있기 때문에 대리자와 이벤트가 같은 위치에 있어야한다고 요구하는 것은 없습니다. 이것이 제가 대답 할 때 "명심하십시오"단락의 의미입니다.
Levi

나는 이것이 혼란스러운 디자인이라는 것을 인정하지만 대안은 더 나빴을 것이다. 결국 당신이 가진 모든 것은 위임입니다 – 당신은 기본 위임을 직접 참조 할 수 있습니다, 당신은 그것을 컬렉션에서 얻을 수 있습니다, 당신은 즉시 그것을 인스턴스화 할 수 있습니다-그것은 "체크 확인 이외의 다른 것을 지원하는 것이 기술적으로 불가능했을 것입니다 null "패턴.
Levi

이벤트 발생에 대해 이야기하면서, 여기에 접근 / 추가 접근자가 중요한 이유를 알 수 없습니다.
Daniel Earwicker

@Levi : C #이 이벤트를 처리하는 방식이 정말 마음에 들지 않습니다. 내가 진실한 사람을 가졌다면, 대의원은 사건과 다른 이름을 받았을 것입니다. 클래스 외부에서 이벤트 이름에 허용되는 유일한 작업은 +=-=입니다. 클래스 내에서 허용되는 작업에는 호출 (내장 null 검사 사용),에 대한 테스트 null또는로 설정하는 것도 포함됩니다 null. 다른 어떤 경우에는 특정 접두사 또는 접미사가있는 이벤트 이름이되는 대리자를 사용해야합니다.
supercat

0

단일 스레드 응용 프로그램의 경우 문제가되지 않습니다.

그러나 이벤트를 노출하는 구성 요소를 만드는 경우 구성 요소 소비자가 멀티 스레딩을하지 않을 것이라는 보장은 없습니다.이 경우 최악의 상황에 대비해야합니다.

빈 델리게이트를 사용하면 문제가 해결되지만 이벤트를 호출 할 때마다 성능이 저하되고 GC에 영향을 줄 수 있습니다.

당신이 이것을하기 위해 소비자가 구독을 취소하는 것이 옳습니다. 그러나 그들이 임시 사본을 지나서 그것을 만들었다면, 이미 전송중인 메시지를 고려하십시오.

임시 변수를 사용하지 않고 빈 대리자를 사용하지 않고 누군가 구독을 취소하면 치명적인 null 참조 예외가 발생하므로 비용이 가치가 있다고 생각합니다.


0

재사용 가능한 구성 요소의 정적 메소드 등에서 이러한 종류의 잠재적 스레딩 불량을 방지하고 정적 이벤트를 만들지 않기 때문에 실제로 이것이 큰 문제라고 생각한 적이 없습니다.

내가 잘못하고 있습니까?


변경 가능한 상태 (값을 변경하는 필드)가있는 클래스의 인스턴스를 할당 한 다음 동시에 두 스레드가 해당 필드를 수정하지 못하도록 잠금을 사용하지 않고 여러 스레드가 동일한 인스턴스에 동시에 액세스 할 수있게하는 경우 아마 잘못하고있을 것입니다. 모든 스레드에 별도의 인스턴스가 있거나 (공유하지 않음) 모든 객체가 변경 불가능한 경우 (한 번 할당되면 필드 값이 변경되지 않음) 괜찮을 것입니다.
Daniel Earwicker

내 일반적인 접근 방식은 정적 메서드를 제외하고 호출자에게 동기화를 유지하는 것입니다. 내가 발신자 인 경우 더 높은 수준에서 동기화합니다. (물론 동기화 된 액세스를 처리하는 목적을 가진 객체를 제외하고. :))
Greg D

방법이 얼마나 복잡한 지와 사용하는 데이터에 따라 달라지는 @GregD. 이 내부 구성원에 영향을, 그리고 당신은 나사 / 임무 상태에서 실행하기로 결정한 경우, 당신은 상처의 많은에에
미키 Perlstein

0

건설중인 모든 행사를 연결하고 혼자 두십시오. 이 게시물의 마지막 단락에서 설명 하듯이 Delegate 클래스의 디자인은 다른 사용법을 올바르게 처리 할 수 ​​없습니다.

우선, 이벤트 핸들러 가 이미 알림에 대한 응답 여부 / 방법에 대해 동기화 된 결정을해야 할 때 이벤트 알림가로 채 려고 할 필요가 없습니다 .

통보 될 수있는 모든 것이 통보되어야합니다. 이벤트 핸들러가 알림을 올바르게 처리하는 경우 (예 : 신뢰할 수있는 애플리케이션 상태에 액세스하고 적절한 경우에만 응답하는 경우) 언제든지 알림을 보내고 올바르게 응답한다고 신뢰하는 것이 좋습니다.

핸들러가 이벤트가 발생했다는 사실을 통지해서는 안되는 것은 이벤트가 실제로 발생하지 않은 경우입니다. 따라서 핸들러에 알리지 않으려면 이벤트 생성을 중지하십시오 (예 : 컨트롤을 비활성화하거나 이벤트를 감지하고 이벤트를 먼저 발생시키는 책임이있는 모든 것을 비활성화하십시오).

솔직히 델리게이트 수업은 구할 수 없다고 생각합니다. MulticastDelegate 로의 합병 / 전환은 이벤트의 (유용한) 정의를 단일 순간에 발생하는 것에서 시간 범위에 걸쳐 발생하는 것으로 효과적으로 변경했기 때문에 큰 실수였습니다. 이러한 변경에는 논리적으로 단일 인스턴트로 다시 축소 할 수있는 동기화 메커니즘이 필요하지만 MulticastDelegate에는 그러한 메커니즘이 없습니다. 동기화는 전체 시간 범위 또는 이벤트가 발생하는 순간을 포함해야하므로 응용 프로그램이 이벤트 처리를 시작하기 위해 동기화 된 결정을 내리면 이벤트 처리가 완전히 완료됩니다 (트랜잭션). MulticastDelegate / Delegate 하이브리드 클래스 인 블랙 박스를 사용하면 거의 불가능하므로단일 구독자를 사용하고 /하거나 처리기 체인을 사용 / 수정하는 동안 제거 할 수있는 동기화 핸들이있는 고유 한 종류의 MulticastDelegate를 구현하십시오 . 대안은 모든 처리기에서 동기화 / 트랜잭션 무결성을 중복으로 구현하는 것이기 때문에 권장합니다. 이는 엄청나게 / 불필요하게 복잡합니다.


[1] "단일 시점"에서 발생하는 유용한 이벤트 핸들러는 없습니다. 모든 작업에는 시간이 있습니다. 모든 단일 핸들러는 수행 할 사소한 일련의 단계를 가질 수 있습니다. 핸들러 목록을 지원하면 아무것도 변경되지 않습니다.
Daniel Earwicker

[2] 행사가 진행되는 동안 자물쇠를 쥐는 것은 광기입니다. 필연적으로 교착 상태로 이어집니다. 소스가 잠금 A를 꺼내고 이벤트가 발생하면 싱크가 잠금 B를 가져옵니다. 이제 두 개의 잠금이 유지됩니다. 다른 스레드에서 작업을 수행하여 잠금이 역순으로 제거되면 어떻게됩니까? 잠금에 대한 책임이 별도로 설계 / 테스트 된 구성 요소 (이벤트의 전체 지점)로 나뉘어 질 때 이러한 치명적인 조합을 어떻게 배제 할 수 있습니까?
Daniel Earwicker

[3] 이러한 문제들 중 어느 것도 단일 스레드 구성 요소의 구성 요소, 특히 GUI 프레임 워크에서 일반적인 멀티 캐스트 델리게이트 / 이벤트의 전체적인 유용성을 감소시키지 않습니다. 이 사용 사례는 대부분의 이벤트 사용을 다룹니다. 자유 스레드 방식으로 이벤트를 사용하는 것은 의문의 여지가 있습니다. 이것은 어떤 식 으로든 디자인이나 의미가있는 상황에서 명백한 유용성을 무효화하지 않습니다.
Daniel Earwicker

[4] 스레드 + 동기 이벤트는 본질적으로 붉은 청어입니다. 대기중인 비동기 통신이 진행됩니다.
Daniel Earwicker

[1] 나는 측정 된 시간을 말하는 것이 아니라 ... 논리적으로 순간적으로 발생하는 원자 연산에 대해 이야기하고 있었으며, 이벤트가 진행되는 동안 사용중인 것과 동일한 리소스를 변경할 수있는 다른 것은 없다는 것을 의미합니다. 잠금으로 직렬화되어 있기 때문입니다.
Triynko 2016 년

0

여기를 참조 하십시오 : http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety 이것은 올바른 해결책이며 다른 해결 방법 대신 항상 사용해야합니다.

“무의미한 익명 메소드로 초기화하여 내부 호출 목록에 항상 하나 이상의 멤버가 있는지 확인할 수 있습니다. 익명의 메서드를 참조 할 수있는 외부 당사자가 없기 때문에 외부 당사자가 메서드를 제거 할 수 없으므로 대리자는 절대 null이되지 않습니다.”— Juval Löwy의 Programming .NET Components, 2 판

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

질문이 C # "event"유형으로 제한되어 있다고 생각하지 않습니다. 이 제한을 없애고 휠을 조금만 다시 발명하고이 라인을 따라 무언가를 해보세요.

이벤트 스레드를 안전하게 올립니다-모범 사례

  • 상승 중일 때 스레드에서 구독 / 구독 취소 기능 (경고 조건 제거)
  • 클래스 수준에서 + = 및-=에 대한 연산자 오버로드
  • 일반 발신자 정의 대리인

0

유용한 토론에 감사드립니다. 나는 최근 에이 문제를 연구하고 약간 느린 클래스를 만들었지 만 폐기 된 객체에 대한 호출을 피할 수 있습니다.

여기서 중요한 점은 이벤트가 발생하더라도 호출 목록을 수정할 수 있다는 것입니다.

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

그리고 사용법은 다음과 같습니다.

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

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

테스트

다음과 같은 방법으로 테스트했습니다. 다음과 같은 객체를 생성하고 파괴하는 스레드가 있습니다.

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

A의 Bar(a 수신기 객체) 생성자 I가 가입 SomeEvent거부에 (상기와 같이 구현되는) 및 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;
    }

또한 루프에서 이벤트를 발생시키는 몇 개의 스레드가 있습니다.

이러한 모든 작업이 동시에 수행됩니다. 많은 리스너가 만들어지고 소멸되며 동시에 이벤트가 시작됩니다.

경쟁 조건이 있으면 콘솔에 메시지가 표시되지만 비어 있습니다. 그러나 평소대로 clr 이벤트를 사용하면 경고 메시지가 가득합니다. 따라서 C #에서 스레드 안전 이벤트를 구현할 수 있다고 결론을 내릴 수 있습니다.

어떻게 생각해?


나에게 충분히 좋아 보인다. 테스트 응용 프로그램에서 disposed = true이전 foo.SomeEvent -= Handler에 발생할 가능성이 있지만 이론적으로 는 오 탐지 가 발생할 수 있다고 생각합니다 . 그러나 그 외에도 몇 가지 변경해야 할 사항이 있습니다. 당신은 정말로 try ... finally자물쇠 에 사용 하기를 원합니다 -이것은 스레드 안전뿐만 아니라 중단 안전에도 도움이됩니다. 당신이 그 바보를 제거 할 수 있다는 것은 말할 것도 없습니다 try catch. 그리고 당신은 Add/에 전달 된 델리게이트를 확인하지 않습니다 Remove-가능합니다 null( Add/에 똑바로 던져야합니다 Remove).
Luaan
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.