Эффективная сигнализация Задачи для завершения TPL при часто повторяющихся событиях

Я работаю над системой моделирования, которая, среди прочего, позволяет выполнять задачи в дискретных симулированных временных шагах. Все выполнение происходит в контексте потока моделирования, но с точки зрения «оператора», использующего систему, они хотят вести себя асинхронно. К счастью, TPL с удобными ключевыми словами «async/await» делает это довольно просто. У меня есть примитивный метод Simulation, подобный этому:

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

Это в основном создание нового TaskCompletionSource и последующий возврат Task. Целью этой задачи является выполнение ее продолжения, когда возникает новый «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 возникает очень часто (примерно каждые несколько миллисекунд, если я работаю на полной скорости). Поскольку эти вспомогательные методы «ожидания» регистрируют новое «ожидание» в каждом цикле, это приводит к большому обороту экземпляров TaskCompletionSource.

Я профилировал свой код и обнаружил, что примерно 5,5% всего процессорного времени тратится на эти завершения, из которых лишь незначительный процент тратится на «активный» код. По сути, все время тратится на регистрацию новых завершений в ожидании выполнения условий срабатывания.

Мой вопрос: как я могу улучшить производительность, сохраняя при этом удобство шаблона async/await для написания «поведения оператора»? Я думаю, мне нужно что-то вроде более легкого и/или многоразового 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;
    }

Это немного забавно, так как ожидающий никогда не сообщает о завершении, но пожары продолжают вызывать завершения как они зарегистрированы; тем не менее, это хорошо работает для этого приложения. Это снижает нагрузку на ЦП с 5,5% до 2,1%. Скорее всего, он все еще потребует некоторой настройки, но это хорошее улучшение по сравнению с оригиналом.

9
задан Dan Bryant 27 June 2012 в 20:42
поделиться