224

최신 정보

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 참조 예외는 다른 스레드가 이벤트에서 상장을 해제하는 경우에만 발생할 수 있으며이를 수행하는 유일한 이유는 해당 기술을 통해 명확하게 달성되지 않은 해당 이벤트를 통해 더 이상 호출을받지 못하도록하는 것입니다. . 당신은 경쟁 조건을 은폐 할 것입니다 - 그것을 밝히는 것이 낫습니다! null 예외는 구성 요소의 남용을 감지하는 데 도움이됩니다. 구성 요소를 악용으로부터 보호하려면 WPF 예제를 따라 생성자에 스레드 ID를 저장 한 다음 다른 스레드가 구성 요소와 직접 상호 작용하려고 시도하면 예외를 throw합니다. 그렇지 않으면 진정한 스레드 안전 구성 요소를 구현할 수 있습니다 (쉬운 작업이 아님).

따라서이 복사 / 확인 관용구를 작성하는 것은화물 컬트 프로그래밍이며 코드에 혼란과 잡음을 추가하는 것이라고 주장합니다. 실제로 다른 스레드로부터 보호하려면 훨씬 더 많은 작업이 필요합니다.

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

따라서 이벤트 핸들러에 대해 놓친 중요한 사항이 있습니다. "이벤트 핸들러는 이벤트가 등록 취소 된 후에도 호출되어야하는 상황에서 견고해야합니다."따라서 분명히 이벤트의 가능성을 신경 써야합니다. 존재를 위임하다null.이벤트 핸들러에 대한 요구 사항은 어디서나 문서화되어 있습니까?

"이 문제를 해결할 수있는 다른 방법이 있습니다 (예 : 제거되지 않는 빈 작업을 처리기로 초기화하는 것).하지만 null 검사를하는 것이 표준 패턴입니다."

그래서 제 질문의 나머지 부분은,명시 적으로 "표준 패턴"을 null-check하는 이유는 무엇입니까?빈 대리자를 할당하는 대신에= delegate {}이벤트 선언에 추가 할 수 있습니다. 이로 인해 이벤트가 제기되는 모든 장소에서 냄새 나는 식의 작은 더미가 제거됩니다. 빈 대리자가 인스턴스화하는 것이 쉽지 않은지 확인하는 것은 쉽습니다. 아니면 아직도 뭔가 빠졌나요?

분명히 그것은 (Jon Skeet이 제안했듯이) 이것이 2005 년에 했어야했듯이 사라지지 않은 .NET 1.x의 조언 일 것임에 틀림 없다.


  • 이 질문은 잠시 동안 내부 토론에서 제기되었습니다. 지금 당분간은 블로그에 올리려고했습니다. 제목에 대한 내 게시물은 여기에 있습니다.이벤트와 레이스 - Eric Lippert
  • Stephen Cleary는CodeProject 기사이 문제를 조사한 그는 "스레드 안전"이라는 일반 목적을 결론 짓는다. 솔루션이 존재하지 않습니다. 기본적으로 대리자가 null이 아니고 이벤트 처리기가 구독 취소 된 후에 호출되도록 처리 할 수 있도록 이벤트 호출자가 처리합니다. - rkagerer
  • @ rkagerer - 실제로 두 번째 문제는 스레드가 관련되지 않은 경우에도 이벤트 처리기에서 처리해야합니다. 한 이벤트 핸들러가 알려주면 발생할 수 있습니다.다른처리기는 현재 처리중인 이벤트의 구독을 취소하지만 두 번째 가입자는 처리 중일 때 구독 취소되었으므로 어쨌든 이벤트를 수신합니다. - Daniel Earwicker
  • 구독자가 0 인 이벤트에 구독을 추가하고, 이벤트에 대한 유일한 구독을 제거하고, 구독자가 0 인 이벤트를 호출하고, 정확히 하나의 구독자로 이벤트를 호출하는 것은 다른 번호가 포함 된 추가 / 제거 / 호출 시나리오보다 훨씬 빠릅니다 가입자. 더미 대리자를 추가하면 일반적인 경우가 느려집니다. C #의 진짜 문제는 제작자가EventName(arguments)null이 아닌 경우 대리자 만 호출하는 대신 이벤트의 대리자를 무조건 호출합니다 (null 인 경우 아무 작업도 수행하지 않음). - supercat

15 답변


97

JIT는 조건 때문에 첫 부분에서 말하는 최적화를 수행 할 수 없습니다. 나는 이것이 얼마 전에 유령으로 제기되었지만 그것이 유효하지 않다는 것을 안다. (얼마 전에 Joe Duffy 나 Vance Morrison과 함께 확인해 보았는데 어느 것을 기억하지 못합니다.)

휘발성 수정자가 없으면 찍은 로컬 복사본의 유효 기간이 만료 될 수도 있지만 그게 전부입니다. 그것은NullReferenceException.

그리고 맞습니다. 확실히 경쟁 조건이 있습니다. 그러나 항상 존재할 것입니다. 코드를 다음과 같이 변경한다고 가정 해보십시오.

TheEvent(this, EventArgs.Empty);

이제 해당 델리게이트의 호출 목록에 1000 개의 항목이 있다고 가정합니다. 다른 스레드가 목록의 끝 부분에서 처리기를 구독 취소하기 전에 목록 시작 부분의 작업이 실행될 가능성이 완전히 있습니다. 그러나 그 핸들러는 새로운 목록이 될 것이기 때문에 여전히 실행될 것입니다. (대의원은 불변이다.) 내가 알 수있는 한, 이것은 피할 수없는 일이다.

빈 대리자를 사용하면 무효 검사를 확실히 피할 수 있지만 경쟁 상태는 해결되지 않습니다. 또한 항상 변수의 최신 값을 "보"게 보장하지는 않습니다.


  • Joe Duffy " Windows에서의 동시 프로그래밍 " 질문의 JIT 최적화 및 메모리 모델 측면을 다룹니다. 만나다code.logos.com/blog/2008/11/events_and_threads_part_4.html - Bradley Grainger
  • " 표준 "에 대한 의견을 토대로이 사실을 수락했습니다. 조언은 C #2 이전이었고 그와 모순되는 말을 듣지 않았습니다. 이벤트 args를 인스턴스화하는 것이 실제로 비용이 많이 드는 경우가 아니면 ' delegate {} ' 이벤트 선언이 끝나면 이벤트를 마치 메소드 인 것처럼 직접 호출하십시오. null을 할당하지 마십시오. (핸들러 보장을 위해 가져온 다른 것들은 상속 취소 후 호출되지 않습니다. 이것은 모두 관련성이 없으며 단일 스레드 코드에 대해서조차도 불가능합니다. 예를 들어 핸들러 1이 핸들러 2에게 델리 스트를 요청하면 핸들러 2는 여전히 호출됩니다 다음 것.) - Daniel Earwicker
  • 유일한 문제는 (항상 그렇듯이) 구조체입니다. 멤버의 null 값 이외의 것으로 인스턴스화 될 수는 없습니다. 하지만 structs 빨아. - Daniel Earwicker
  • 빈 대리인 정보,이 질문도 참조하십시오 :stackoverflow.com/questions/170907/…. - Vladimir
  • @ 토니 : 근본적으로 가입 / 탈퇴와 대리인이 실행되는 사이의 경쟁 조건이 여전히 있습니다. 코드를 잠깐 살펴보고 나면 코드를 올리는 동안 구독 / 가입 취소를 적용 할 수 있으므로 해당 경쟁 조건이 줄어들지 만 대부분의 경우 정상적인 동작이 충분하지 않은 것으로 판단됩니다. 이 중 하나도 아닙니다. - Jon Skeet

50

이 일을하는 확장 방법으로가는 사람들이 많이 있다는 것을 알았습니다 ...

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

메서드 호출시 캡처되기 때문에 로컬 복사본도 제거합니다.


  • 멋진 구문! 그 즉시 사용을 시작하겠습니다. - Jason Coyne
  • 문법이 마음에 들지만 분명히 해두면 부실 처리기가 등록 해제 된 후에도 호출되는 문제를 해결하지 못합니다. 이널 참조 취소 문제를 해결합니다. 구문이 마음에 들지만 실제로는 다음과 같은 것이 아닌지 질문합니다. public Event EventHandler < T > MyEvent = delete {}; ... MyEvent (this, new MyEventArgs ()); 이것은 또한 단순성 때문에 내가 좋아하는 매우 낮은 마찰 솔루션입니다. - Simon Gillbee
  • @ 사이먼 나는 이것에 대해 다른 것을 말하는 다른 사람들을 만난다. 테스트 한 결과 null 처리기 문제를 처리한다는 사실을 알았습니다. 처리기! = null 검사 후 원래 싱크가 이벤트에서 등록을 취소하더라도 이벤트는 여전히 발생하고 예외는 발생하지 않습니다. - JP Alioto
  • 그래,이 질문에 :stackoverflow.com/questions/192980/… - Benjol
  • +1. 방금 스레드 안전에 대해 생각하기 시작하면서,이 방법을 직접 작성했습니다. 몇 가지 조사를하고이 질문을 발견했습니다. - Niels van der Rest

31

"왜 명시 적으로 '표준 패턴'을 널 체크하지 않습니까?"

나는 이것에 대한 이유가 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 이벤트 호출 스레드 안전성나는이 질문이 나오기 바로 전에 그 날을 출판했다. (!)

(내 테스트 설정에 결함이있어 소스 코드를 다운로드하여 직접 조사해보십시오. 모든 의견을 보내 주시면 감사하겠습니다.)


  • 블로그 게시글에서 핵심 포인트를 만들었다 고 생각합니다. 병목 현상이 발생할 때까지 성능에 대한 영향을 걱정할 필요가 없습니다. 왜 추한 방법을 권장 방법으로 삼습니까? 우리가 명확하지 않고 조숙 한 최적화를 원한다면 어셈블러를 사용하게 될 것입니다. 따라서 제 질문은 여전히 남아 있습니다. 아마도이 조언은 익명의 대리인보다 앞서서 인간 문화가 오래된 조언을 바꾸는 데 오랜 시간이 걸릴 것입니다 유명한 "냄비 로스트 이야기 (pot roast story)"에서처럼. - Daniel Earwicker
  • 그리고 당신의 수치는 그 점을 매우 잘 보여줍니다. 오버 헤드는 이벤트 당 발생하는 NANOSECONDS (!!!)와 (초기 이전 vs. 실제 작업을 수행하는 거의 모든 응용 프로그램에서는이 기능을 사용할 수 없지만 대부분의 이벤트 사용이 GUI 프레임 워크에 있다고 가정하면 Winforms 등에서 화면의 일부를 다시 칠하는 비용과 비교해야합니다. 따라서 실제 CPU 작업의 대홍수와 리소스를 기다리는 중에는 더 이상 보이지 않게됩니다. 어쨌든, 열심히 일하면 +1을 얻을 수 있습니다. :) - Daniel Earwicker
  • @ DanielEarwicker가 말했습니다. 공개 이벤트를 신봉하는 사람으로 이동했습니다. WrapperDoneHandler OnWrapperDone = (x, y) = > {}; 모델. - Mickey Perlstein
  • 시간을 보내는 것도 좋습니다.Delegate.Combine/Delegate.Remove이벤트에 0, 1 또는 2 명의 가입자가있는 경우 쌍; 동일한 대리인 인스턴스를 반복적으로 추가하고 제거하는 경우에는 사례 간 비용 차이가 특히 두드러 질 것입니다.Combine인수 중 하나가 다음과 같은 경우 빠른 특수 사례 동작이 있습니다.null(그냥 다른 하나를 반환), 그리고Remove두 인수가 같을 때 매우 빠릅니다 (null 만 반환). - supercat

10

나는이 읽기를 정말로 즐겼다 - 그렇지 않다! 이벤트라고하는 C #기능을 사용하려면이 기능이 필요합니다.

왜 컴파일러에서 이것을 고치지 않습니까? 나는이 게시물을 읽은 MS 사람들이 있다는 것을 알고 있습니다.

1 - Null 문제) 왜 이벤트가 처음부터 null 대신에 비어있게 만드나요? 널 체크를 위해 얼마나 많은 코드 라인을 저장할 것인가?= delegate {}선언문에? 컴파일러가 Empty case를 처리하도록하십시오. IE는 아무 것도하지 않습니다! 사건의 창시자에게 모든 것이 중요하다면, 그들은 비어있는 곳을 확인하고 그것을 돌보는 일을 할 수 있습니다! 그렇지 않으면 모든 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는 생성시 잠금 플래그를 설정하여 호출 될 때마다 잠금을 설정하고 등록을 취소합니다.

결론,

현대의 언어로 이런 문제를 해결할 수 있습니까?


  • 동의, 컴파일러에서 더 나은 지원이 있어야합니다. 그때까지, 나는post-compile 단계에서 PostSharp 애스펙트를 생성했습니다.. :) - Steven Jeuris
  • 임의의 외부 코드가 완료되기를 기다리는 동안 스레드 가입 / 탈퇴 요청 차단이 발생했습니다.훨씬 더구독자가 구독이 취소 된 후에 이벤트를 수신하는 것보다, 특히 후자의 "문제" 해결할 수있다.용이하게단순히 이벤트 핸들러가 플래그를 확인하여 이벤트 수신에 여전히 관심이 있는지 확인하지만 이전 디자인으로 인한 교착 상태는 다루기 어려울 수 있습니다. - supercat
  • @supercat. Imo, "훨씬 더 나쁨" 주석은 꽤 복잡한 응용 프로그램입니다. 옵션이있을 때 추가 플래그없이 매우 엄격한 잠금을 원하는 사람이 누구입니까? 교착 상태는 잠금이 동일한 스레드 재진입이고 원본 이벤트 핸들러 내의 가입 / 탈퇴가 차단되지 않으므로 이벤트 처리 스레드가 다른 스레드 (서브 스크립 션 / 가입 해제)에서 대기중인 경우에만 발생해야합니다. 디자인의 일부가 될 이벤트 핸들러의 일부로 대기하는 크로스 스레드가있는 경우 재 작업을 선호합니다. 예측 가능한 패턴이있는 서버 측 앱 각도에서 왔습니다. - crokusek
  • @crokusek : 필요한 분석알다시스템이 교착 상태가 없다는 것은 각 잠금을 모든 잠금에 연결하는 방향 그래프에 순환이 없으면 쉽습니다[사이클의 부족으로 인해 시스템이 교착 상태가되지 않음을 증명합니다]. 로크가 유지되는 동안 임의의 코드가 호출 될 수있게하면, "필요성이 있음"필드에 에지가 생성된다. 그 잠금에서 임의의 코드가 획득 할 수있는 모든 잠금 (시스템의 모든 잠금이 아니라 멀리 떨어져있는 잠금)에 대한 그래프. 사이클의 결과로 교착 상태가 발생한다는 것을 암시하지 않지만 ... - supercat
  • ... 분석 할 수 없다는 것을 증명하는 데 필요한 분석 수준을 크게 높일 것입니다. - supercat

5

제프리 리히터 (Jeffrey Richter)에 따르면C #을 통한 CLR올바른 방법은 다음과 같습니다.

// 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을 던집니다.msdn.microsoft.com/en-us/library/bb297966.aspx - Kniganapolke
  • Interlocked.CompareExchange그것이 어떻게 든 통과했다면 실패 할 것이다.ref, 그러나 그것은 전달되는 것과 같은 것이 아닙니다.ref저장 위치 (예 :NewMail)이 존재하고 처음에는보류하다null 참조. - 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);
  }
}

요즘 안드로이드 용 Mono와 주로 작업 중이며, Activity가 백그라운드로 보내진 후에 View를 업데이트하려고 할 때 Android가 좋아 보이지 않는 것 같습니다.



1

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

경쟁 참조가 아닌 null 참조 예외에 대해 신경을 쓰는 사람들 뒤에있는 추론은 심층적 인 심리 연구가 필요합니다. null 참조 문제를 수정하는 것이 훨씬 쉽다는 사실과 관련이 있다고 생각합니다. 일단 수정되면, 그들은 큰 "Mission Accomplished"배너를 코드에 걸어 놓고 그들의 비행복을 압축 해제합니다.

참고 : 경쟁 조건을 수정하는 것은 아마도 동기 플래그 트랙을 사용하여 처리기를 실행해야하는지


  • 문제에 대한 해결책을 묻지 않습니다. 이벤트 발사와 관련하여 추가 코드를 스프레이하는 광범위한 조언이 왜 있는지 궁금합니다. 감지하기 어려운 경합 조건이 존재할 때만 null 예외를 피할 수 있습니다. 이는 여전히 존재합니다. - Daniel Earwicker
  • 그게 내 요점 이었어. 그들은 돈을받지 못한다.어느경쟁 조건에 대해서. 그들은 오직 null 참조 예외에만 관심이 있습니다. 나는 그것을 나의 대답으로 편집 할 것이다. - dss539
  • 그리고 요점은 null 참조 예외에 대해 신경 쓰는 것이 왜아니경쟁 조건에 관심을 가져라. - Daniel Earwicker
  • 올바르게 작성된 이벤트 핸들러는 처리가 추가 또는 제거 요청과 겹칠 수있는 이벤트를 발생시키는 특정 요청이 추가 또는 제거 된 이벤트를 발생시킬 수도 있고 끝내지 않을 수도 있다는 사실을 처리 할 준비가되어 있어야합니다. 프로그래머가 경쟁 조건에 신경 쓰지 않는 이유는 코드가 올바로 작성된 것입니다누가이기 든 상관 없다.. - supercat
  • @ dss539 : 보류중인 이벤트 호출이 끝날 때까지 가입 취소 요청을 차단할 수있는 이벤트 프레임 워크를 설계 할 수 있지만, 그러한 디자인은 모든 이벤트 (예 :Unload이벤트)를 사용하여 다른 이벤트에 대한 개체의 구독을 안전하게 취소 할 수 있습니다. 추잡한. 이벤트 구독 취소 요청으로 인해 이벤트가 "결국"구독 취소되고 이벤트 구독자는 이벤트가 유용한 지 여부를 확인해야합니다. - supercat

1

파티에 좀 늦었어요. :)

구독자가없는 이벤트를 나타 내기 위해 null 객체 패턴 대신 null을 사용하는 경우이 시나리오를 고려하십시오. 이벤트를 호출해야하지만 객체 (EventArgs)를 생성하는 것은 간단합니다. 일반적으로 이벤트에는 구독자가 없습니다. 인수를 작성하고 이벤트를 호출하기 전에 처리 노력을 투입하기 전에 구독자가 있는지 확인하기 위해 코드를 최적화 할 수 있다면 유용 할 것입니다.

이를 염두에두고 해결책은 "글쎄, 제로 가입자는 null로 표시됩니다."라고 말하는 것입니다. 그런 다음 값 비싼 작업을 수행하기 전에 null 확인을 수행하십시오. 이 작업을 수행하는 또 다른 방법은 위임 유형에 Count 속성을 사용 했으므로 myDelegate.Count가 0보다 많은 경우 비싼 작업 만 수행 할 수 있습니다. Count 속성을 사용하면 원래 문제를 해결하는 다소 좋은 패턴입니다. 최적화를 허용하고 NullReferenceException을 발생시키지 않고 호출 할 수있는 좋은 속성을 가지고 있습니다.

하지만 델리게이트는 참조 유형이므로 null이 될 수 있습니다. 아마도이 사실을 숨기고 이벤트에 대한 null 객체 패턴 만 지원하는 좋은 방법이 없었을 것입니다. 따라서 대안은 개발자가 null과 가입자가없는 것을 모두 확인하도록 강요되었을 수 있습니다. 그것은 현재 상황보다 더 추악 할 것이다.

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


  • 나는 "빈 대리인을 사용하는 것보다 ..."을 의미한다고 가정합니다. 빈 대리자로 초기화 된 이벤트로 이미 제안한 것을 수행 할 수 있습니다. 초기 빈 대리자가 목록에있는 유일한 경우 테스트 (MyEvent.GetInvocationList (). Length == 1)가 true가됩니다. 사본을 먼저 만들 필요는 없습니다. 어쨌든 당신이 묘사 한 사건은 극히 드물 것이라고 생각합니다. - Daniel Earwicker
  • 저는 대표자와 행사의 아이디어를 여기에 모아 놓은 것 같습니다. 내 수업에 Foo 이벤트가있는 경우 외부 사용자가 MyType.Foo + = / - =를 호출하면 실제로 add_Foo () 및 remove_Foo () 메소드가 호출됩니다. 그러나 Foo가 정의 된 클래스에서 Foo를 참조 할 때 실제로 add_Foo () 및 remove_Foo () 메서드가 아니라 기본 대리자를 직접 참조합니다. EventHandlerList와 같은 유형이 존재하기 때문에 대리인과 이벤트가 같은 위치에있을 것을 요구하는 것은 없습니다. 이것은 내가 "Keep in mind (마음에 간직하십시오)"라는 의미입니다. 단락 내 대답. - Levi
  • (계속)이 디자인이 혼란 스럽다는 것을 인정하지만 대안이 더 나쁠 수도 있습니다. 결국 당신이 가지고있는 것은 모두 대리인입니다. 즉, 기본 대리자를 직접 참조 할 수도 있고 컬렉션에서 가져올 수도 있습니다. 즉석에서 인스턴스화 할 수 있습니다. "확인"이외의 다른 것을 지원하는 것은 기술적으로 불가능합니다. null의 경우 " 무늬. - Levi
  • 이벤트 실행과 관련하여 여기서는 추가 / 제거 접근자가 왜 중요한지 알 수 없습니다. - Daniel Earwicker
  • @ 리비 : C #이 이벤트를 처리하는 방식을 정말 싫어합니다. 내 druthers 있었다면, 대표는 이벤트에서 다른 이름을 부여되었을 것입니다. 클래스 외부에서 이벤트 이름에 대한 유일한 허용 작업은 다음과 같습니다.+=-=. 클래스 내에서 허용되는 작업에는 호출 (내장 된 null 검사 포함), 테스트null, 또는로 설정null. 다른 어떤 경우에는 이름이 특정 접두사 또는 접미사가있는 이벤트 이름이 될 대리자를 사용해야합니다. - supercat

1

C #6 이상에서는 새로운 코드를 사용하여 코드를 단순화 할 수있었습니다..? operator~ 같이TheEvent?.Invoke(this, EventArgs.Empty);

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-conditional-operators


0

단일 스레드 응용 프로그램의 경우, 이것이 문제가 아닙니다.

그러나 이벤트를 노출하는 구성 요소를 만드는 경우 구성 요소의 사용자가 멀티 스레딩을 수행하지 않는다고 보장 할 수 없으며 최악의 상황에 대비해야합니다.

빈 대리자를 사용하면 문제를 해결할 수 있지만 이벤트 호출마다 성능이 저하되고 GC 관련 문제가 발생할 수 있습니다.

이 문제가 발생하기 위해서는 소비자가 탈퇴해야하지만, 임시 복사본을 지나친 경우에는 이미 전송중인 메시지를 고려하십시오.

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


0

필자는 일반적으로 재사용 가능한 구성 요소의 정적 메서드 (등)에서 이러한 잠재적 인 스레딩 문제를 방지하기 때문에이 문제를별로 고려하지 않았으며 정적 이벤트를 만들지 않습니다.

내가 잘못하고 있니?


  • mutable 상태 (값을 변경하는 필드)가있는 클래스의 인스턴스를 할당 한 다음 동시에 두 스레드가 수정하지 않도록 해당 필드를 보호하기 위해 잠금을 사용하지 않고 여러 인스턴스가 동시에 동일한 인스턴스에 액세스하게하려면, 아마도 잘못했을 수도 있습니다. 모든 스레드가 별도의 인스턴스를 가지고 있거나 (아무것도 공유하지 않은 경우) 또는 모든 개체를 변경할 수 없으면 (할당 된 필드의 값은 절대로 변경되지 않습니다.) 아마 괜찮아 질 것입니다. - Daniel Earwicker
  • 내 일반적인 접근 방식은 정적 메서드를 제외하고는 호출자에게 동기화를 맡기는 것입니다. 발신자 인 경우 해당 상위 수준에서 동기화됩니다. (물론 유일한 목적은 동기화 된 액세스를 처리하는 객체를 제외하고는 :) :)) - Greg D
  • @GregD는 방법이 얼마나 복잡한 지와 사용되는 데이터에 따라 다릅니다. 내부 구성원에게 영향을 미치고 스레드 / 작업 상태에서 실행하기로 결정하면 많은 상처를 입을 수 있습니다 - Mickey Perlstein

0

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

우선, 시도하는 데 아무런 의미가 없습니다.요격하다이벤트공고너의 사건핸들러여부에 관한 동기화 된 결정을 이미 내려야합니다.통보에 응답하다..

통보를받을 수있는 사항은 모두 통보해야합니다. 이벤트 처리기가 알림을 제대로 처리하고있는 경우 (즉, 권한있는 애플리케이션 상태에 액세스하여 적절한 경우에만 응답하는 경우) 언제든지 알림을 보내고 올바르게 응답 할 것입니다.

핸들러가 이벤트가 발생했다는 통지를받지 않는 유일한 시간은 이벤트가 실제로 발생하지 않았다는 것입니다. 따라서 처리기에 알림을 보내지 않으려면 이벤트 생성을 중지하십시오 (즉, 컨트롤을 비활성화하거나 이벤트를 감지하여 처음부터 가져 오는 책임은 무엇이든).

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


  • [1] "단일 인스턴트 타임"에서 발생하는 유용한 이벤트 핸들러는 존재하지 않는다. 모든 작업에는 timespan이 있습니다. 모든 단일 핸들러는 수행해야 할 단계의 순서가 중요하지 않을 수 있습니다. 핸들러 목록을 지원하면 아무 것도 변경되지 않습니다. - Daniel Earwicker
  • [2] 사건이 해고되는 동안 자물쇠를 들고있는 것은 총 광기입니다. 필연적으로 교착 상태가됩니다. 소스는 잠금 A를 가져오고, 이벤트는 발생하고, 싱크는 잠금 B를 제거합니다. 이제 두 개의 잠금이 유지됩니다. 다른 thread의 조작에 의해, 역순으로 락이 꺼내지는 경우는 어떻게됩니까? 잠금에 대한 책임이 별도로 설계 / 테스트 된 구성 요소 (전체 이벤트 지점)로 나누어지면 그러한 치명적인 조합은 어떻게 배제 될 수 있습니까? - Daniel Earwicker
  • [3] 이러한 문제로 인해 단일 스레드 구성 요소, 특히 GUI 프레임 워크에서 일반적인 멀티 캐스트 대리자 / 이벤트가 널리 보급되지 않습니다. 이 사용 사례는 대다수의 이벤트 사용을 다룹니다. 자유 스레드 방식으로 이벤트를 사용하는 것은 의문의 여지가 있습니다. 이것은 그들이 의미가있는 상황에서 어떤 식 으로든 그들의 디자인이나 명백한 유용성을 무효로 만들지 않습니다. - Daniel Earwicker
  • [4] 스레드 + 동기 이벤트는 본질적으로 붉은 청어입니다. 비동기식 대기열 통신은 갈 길입니다. - Daniel Earwicker
  • [1] 측정 된 시간을 언급하지 않았습니다. 논리적으로 순간적으로 발생하는 원자 적 작동에 대해 이야기하고 있었고, 사용하는 동일한 자원을 사용하는 다른 작업은 변경할 수 없음을 의미합니다. 이벤트는 잠금 장치로 직렬화되어 있기 때문에 발생합니다. - Triynko

0

여기 좀보세요 :http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety이것은 올바른 해결책이며 다른 모든 대안 대신 항상 사용해야합니다.

"내부 호출 목록에는 아무것도하지 않는 무의미한 메서드로 초기화하여 멤버가 적어도 하나 이상 있는지 확인할 수 있습니다. 외부 당사자가 익명 메서드에 대한 참조를 가질 수 없으므로 외부 공급자가 메서드를 제거 할 수 없으므로 대리자가 null이 될 수 없습니다 "     - Juval Löwy의 .NET 구성 요소 프로그래밍, 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 #"이벤트"유형에 제한적이라고 생각하지 않습니다. 그 제한을 없애고 바퀴를 약간 다시 발명하고이 선을 따라 뭔가를할까요?

이벤트 스레드 안전하게 보호 - 모범 사례

  • 레이즈 (레이스) 중에 어떤 스레드에서 서브 / 서브에서 탈퇴 할 수있는 능력 제거 된 상태)
  • 연산자가 클래스 수준에서 + = 및 - =에 대해 오버로드됩니다.
  • 일반 호출자 정의 대리자


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

안에Bar구독자 (수신기 개체) 생성자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

연결된 질문


관련된 질문

최근 질문