11

私は、とりわけ、個別のシミュレートされた時間ステップでタスクを実行できるシミュレーションシステムに取り組んでいます。実行はすべてシミュレーションスレッドのコンテキストで行われますが、システムを使用する「オペレーター」の観点からは、非同期で動作することを望んでいます。ありがたいことに、便利な「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%に削減されます。それでも多少の調整が必要になる可能性がありますが、元のバージョンよりも優れています。

4

3 に答える 3

9

awaitキーワードはsだけで機能するのではなく、Task待機可能なパターンに従うものすべてで機能します。詳細については、StephenToubの記事を参照してください

短いバージョンでは、型にはGetAwaiter()、実装する型を返すメソッドが必要であり、プロパティとメソッドINotifyCompletionも含まれている必要があります(式に値がない場合は、-returning )。例については、を参照してください。IsCompletedGetResult()voidawaitTaskAwaiter

独自の待機可能ファイルを作成すると、毎回同じオブジェクトを返すことができ、多くTaskCompletionSourceのを割り当てるオーバーヘッドを回避できます。

于 2012-06-27T19:37:25.077 に答える
5

これが私のバージョンのReusableAwaiterシミュレーションです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;
    }
}

そして、これがテストコードです。

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

}
于 2016-11-19T04:26:51.583 に答える
0

本当にWaitForDuration別のスレッドで-eventを受け取る必要がありますか?そうでない場合は、コールバック(またはイベント)をに登録して、_cycleExecutedBroker同期的に通知を受け取ることができます。コールバックでは、任意の条件をテストでき、その条件がtrueであることが判明した場合にのみ、別のスレッドに通知します(タスク、メッセージ、またはその他のメカニズムを使用)。テストする条件がtrueと評価されることはめったにないことを理解しているので、ほとんどのクロススレッド呼び出しをそのように回避します。

私の答えの要点は次のとおりです。計算を「ソース」スレッドに移動することにより、クロススレッドメッセージングの量を減らしてみてください。

于 2012-06-27T18:51:35.047 に答える