私は、とりわけ、個別のシミュレートされた時間ステップでタスクを実行できるシミュレーションシステムに取り組んでいます。実行はすべてシミュレーションスレッドのコンテキストで行われますが、システムを使用する「オペレーター」の観点からは、非同期で動作することを望んでいます。ありがたいことに、便利な「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;
}
ウェイターが完了を報告することはないので、少しおかしいですが、登録されている間、firesは完了を呼び出し続けます。それでも、このアプリケーションではうまく機能します。これにより、CPUオーバーヘッドが5.5%から2.1%に削減されます。それでも多少の調整が必要になる可能性がありますが、元のバージョンよりも優れています。