2012-06-27 5 views
6

다른 것들 중에서 이산 시뮬레이션 된 시간 단계로 작업을 실행할 수있는 시뮬레이션 시스템을 만들고 있습니다. 실행은 모두 시뮬레이션 스레드의 컨텍스트에서 발생하지만 시스템을 사용하는 '연산자'의 관점에서 비동기 적으로 동작하려고합니다. 고맙게도 TPL은 편리한 'async/await'키워드를 사용하여이를 매우 간단하게 만듭니다.효율적인 시그널링 자주 재발행하는 이벤트에 대한 TPL 완료를위한 작업

public Task CycleExecutedEvent() 
    { 
     lock (_cycleExecutedBroker) 
     { 
      if (!IsRunning) throw new TaskCanceledException("Simulation has been stopped"); 
      return _cycleExecutedBroker.RegisterForCompletion(CycleExecutedEventName); 
     } 
    } 

이 기본적으로 새로운 TaskCompletionSource을 만든 다음 작업을 반환 :이 같은 시뮬레이션에 원시적 인 방법이있다. 이 태스크의 목적은 시뮬레이션에서 새로운 'ExecuteCycle'이 발생할 때 계속 실행하는 것입니다.

나는 다음과 같은 몇 가지 확장 방법이 있습니다

public static async Task WaitForDuration(this ISimulation simulation, double duration) 
    { 
     double startTime = simulation.CurrentSimulatedTime; 
     do 
     { 
      await simulation.CycleExecutedEvent(); 
     } while ((simulation.CurrentSimulatedTime - startTime) < duration); 
    } 

    public static async Task WaitForCondition(this ISimulation simulation, Func<bool> condition) 
    { 
     do 
     { 
      await simulation.CycleExecutedEvent(); 
     } while (!condition()); 
    } 

이가에 '운영자 관점에서 시퀀스를 구축 조건에 따라 조치를 복용하고 시뮬레이션 시간의 기간 동안 대기를 들어, 다음, 매우 편리합니다. 내가 겪고있는 문제는 CycleExecuted가 매우 자주 발생한다는 것입니다 (완전히 가속 된 속도로 실행하는 경우 대략 몇 밀리 초마다 발생합니다). 이러한 'wait'도우미 메서드는 각주기마다 새로운 'await'을 등록하기 때문에 TaskCompletionSource 인스턴스에서 많은 매출을 발생시킵니다.

필자는 코드를 프로파일 링 했으므로 총 CPU 시간의 약 5.5 %가 이러한 완료 내에서 소비되었음을 알았습니다.이 중 '활성'코드에는 무시할만한 비율 만 사용됩니다. 효과적으로 트리거 조건이 유효하기를 기다리는 동안 새 완료를 등록하는 데는 항상 시간이 소비됩니다.

내 질문 : '운영자 행동 양식'을 작성하기위한 비동기/대기 패턴의 편의성을 유지하면서 여기 성능을 어떻게 향상시킬 수 있습니까? 트리거링 이벤트가 너무 자주 발생하기 때문에 경량 및/또는 재사용 가능한 TaskCompletionSource와 같은 것이 필요하다고 생각합니다.


의 무리에 대한 필요성을 제거, 나는 조금 더 연구를하고 봤는데 좋은 옵션이 행사에 직접 묶을 수있는 Awaitable 패턴의 사용자 지정 구현을 만들 수있을 것 같은데 TaskCompletionSource 및 Task 인스턴스. 여기에서 유용 할 수있는 이유는 CycleExecutedEvent를 기다리는 많은 연속성이 있으며 자주 기다려야하기 때문입니다. 이상적으로는 연속 콜백을 큐에 대기시킨 다음 이벤트가 발생할 때마다 큐의 모든 것을 다시 호출하는 방법을 찾고 있습니다. 나는 파기를 계속할 것이지만 사람들이 이것을하기위한 깔끔한 방법을 안다면 어떤 도움이라도 환영한다.

public sealed class CycleExecutedAwaiter : INotifyCompletion 
{ 
    private readonly List<Action> _continuations = new List<Action>(); 

    public bool IsCompleted 
    { 
     get { return false; } 
    } 

    public void GetResult() 
    { 
    } 

    public void OnCompleted(Action continuation) 
    { 
     _continuations.Add(continuation); 
    } 

    public void RunContinuations() 
    { 
     var continuations = _continuations.ToArray(); 
     _continuations.Clear(); 
     foreach (var continuation in continuations) 
      continuation(); 
    } 

    public CycleExecutedAwaiter GetAwaiter() 
    { 
     return this; 
    } 
} 

을 그리고 시뮬레이터 :

private readonly CycleExecutedAwaiter _cycleExecutedAwaiter = new CycleExecutedAwaiter(); 

    public CycleExecutedAwaiter CycleExecutedEvent() 
    { 
     if (!IsRunning) throw new TaskCanceledException("Simulation has been stopped"); 
     return _cycleExecutedAwaiter; 
    } 

그것은으로, 조금 재미 나는 함께 넣어 awaiter 앞으로이 문제를 탐색하는 사람을 위해


, 여기에 사용자 정의입니다 대기자는 Complete를보고하지 않지만 화재는 등록 된대로 완료를 호출합니다. 여전히이 응용 프로그램에서 잘 작동합니다. 이렇게하면 5.5 %에서 2.1 %로 CPU 오버 헤드를 줄일 수 있습니다. 그것은 여전히 ​​약간의 조정이 필요할 것 같지만 원본보다 좋은 개선입니다.

+1

안녕하세요. 나에게 1 분 전에 질문에 대답하는 것이 공평하지 않습니다. :-) – svick

+0

@svick, 아직 완전히 답변되지 않았습니다. 나는 주문 습관을 만드는 방법을 알아낼 필요가있다. :) 링크 주셔서 감사합니다; 그것들은 아주 도움이됩니다. –

+0

@DanBryant : 질문에 대답해야합니다 ... 자신의 질문 본문이 아니라 답변으로 대답해야합니다. – user7116

답변

5

await 키워드는 Task에 대해서만 작동하며 기다릴 수있는 패턴을 따르는 모든 키워드에서 작동합니다. 자세한 내용은 Stephen Toub's article await anything;을 참조하십시오.

짧은 버전 타입 INotifyCompletion을 구현해합니다 (await 식 값을 안 경우 void -returning) IsCompleted 건물 GetResult() 방법을 갖는 타입을 반환하는 방법 GetAwaiter()를 가져야한다는 것이다. 예를 들어 TaskAwaiter을 참조하십시오.

기다리는 시간을 직접 작성하면 매번 동일한 객체를 반환하여 많은 수의 TaskCompletionSource을 할당하는 오버 헤드를 피할 수 있습니다.

+0

내가 제안한대로 맞춤식 대기열을 만들었습니다. 훨씬 더 빠르지 만 여전히 개선의 여지가 있지만 확실합니다. 버그에 대해 저에게 중요한 점은, 외부 클래스가 Awaiter 인스턴스를 잡고 RunContinuations를 호출하여 예상 된 컨텍스트 외부로 트리거 할 수 있다는 것입니다. –

+0

@DanBryant 저는 그것에 대해 아무 것도 할 수 있을지 확신하지 못합니다. 누군가에게 당신이 원하는대로 연속체를 스케쥴하는 방법을 제공한다면, 그들은 예상 된 상황 밖에서 언제나 그것을 사용할 수 있습니다. – svick

0

다른 스레드에서 WaitForDuration - 이벤트를 실제로 받아야합니까? 그렇지 않은 경우 _cycleExecutedBroker이라는 콜백 (또는 이벤트)을 등록하고 알림을 동 기적으로 수신 할 수 있습니다.콜백에서 원하는 조건을 테스트 할 수 있습니다. 조건이 참인 경우에만 작업, 메시지 또는 기타 메커니즘을 사용하여 다른 스레드에 알립니다. 테스트 할 조건이 사실이라고 평가하는 경우는 드물기 때문에 대부분의 크로스 스레드 호출을 피할 수 있습니다.

내 대답은 다음과 같습니다. "원본"스레드로 계산을 이동하여 크로스 스레드 메시징의 양을 줄이십시오.

+0

실제로 스레드 경계를 넘지 않습니다. TaskCompletionSource는 시뮬레이션 스레드에서 완료로 표시되기 때문에 모든 완료는 시뮬레이션 스레드에서 실행됩니다. 이 경우 비동기/대기를 사용하는 전체 요점은 동일한 스레드에서 모든 작업을 계속 실행하고 제어 된 '수익률'은 시뮬레이션 된 실행주기마다 한 번만 확인하는 것입니다. –

+0

그래도 여전히 작업에는 간단한 이벤트가 발생하지 않는 오버 헤드가 있습니다. 라이프 사이클 동안 하드웨어 잠금과 매우 비싼 여러 개의 인터 로킹 된 작업이 필요합니다. 그것들은 시스템 글로벌 리소스입니다. – usr

1

여기 TaskCompletionSource

public sealed class ReusableAwaiter<T> : INotifyCompletion 
{ 
    private Action _continuation = null; 
    private T _result = default(T); 
    private Exception _exception = null; 

    public bool IsCompleted 
    { 
     get; 
     private set; 
    } 

    public T GetResult() 
    { 
     if (_exception != null) 
      throw _exception; 
     return _result; 
    } 

    public void OnCompleted(Action continuation) 
    { 
     if (_continuation != null) 
      throw new InvalidOperationException("This ReusableAwaiter instance has already been listened"); 
     _continuation = continuation; 
    } 

    /// <summary> 
    /// Attempts to transition the completion state. 
    /// </summary> 
    /// <param name="result"></param> 
    /// <returns></returns> 
    public bool TrySetResult(T result) 
    { 
     if (!this.IsCompleted) 
     { 
      this.IsCompleted = true; 
      this._result = result; 

      if (_continuation != null) 
       _continuation(); 
      return true; 
     } 
     return false; 
    } 

    /// <summary> 
    /// Attempts to transition the exception state. 
    /// </summary> 
    /// <param name="result"></param> 
    /// <returns></returns> 
    public bool TrySetException(Exception exception) 
    { 
     if (!this.IsCompleted) 
     { 
      this.IsCompleted = true; 
      this._exception = exception; 

      if (_continuation != null) 
       _continuation(); 
      return true; 
     } 
     return false; 
    } 

    /// <summary> 
    /// Reset the awaiter to initial status 
    /// </summary> 
    /// <returns></returns> 
    public ReusableAwaiter<T> Reset() 
    { 
     this._result = default(T); 
     this._continuation = null; 
     this._exception = null; 
     this.IsCompleted = false; 
     return this; 
    } 

    public ReusableAwaiter<T> GetAwaiter() 
    { 
     return this; 
    } 
} 

을 시뮬레이션 ReusableAwaiter의 내 버전입니다 그리고 여기 테스트 코드입니다.

class Program 
{ 
    static readonly ReusableAwaiter<int> _awaiter = new ReusableAwaiter<int>(); 

    static void Main(string[] args) 
    { 
     Task.Run(() => Test()); 

     Console.ReadLine(); 
     _awaiter.TrySetResult(22); 
     Console.ReadLine(); 
     _awaiter.TrySetException(new Exception("ERR")); 

     Console.ReadLine(); 
    } 

    static async void Test() 
    { 

     int a = await AsyncMethod(); 
     Console.WriteLine(a); 
     try 
     { 
      await AsyncMethod(); 
     } 
     catch(Exception ex) 
     { 
      Console.WriteLine(ex.Message); 
     } 

    } 

    static ReusableAwaiter<int> AsyncMethod() 
    { 
     return _awaiter.Reset(); 
    } 

}