Я работаю над системой моделирования, которая, среди прочего, позволяет выполнять задачи в дискретных симулированных временных шагах. Все выполнение происходит в контексте потока моделирования, но с точки зрения «оператора», использующего систему, они хотят вести себя асинхронно. К счастью, 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%. Скорее всего, он все еще потребует некоторой настройки, но это хорошее улучшение по сравнению с оригиналом.