Sto lavorando a un sistema di simulazione che, tra le altre cose, consente l'esecuzione di compiti in fasi temporali simulate. L'esecuzione avviene tutto nel contesto del thread di simulazione, ma, dal punto di vista di un "operatore" che utilizza il sistema, desidera comportarsi in modo asincrono. Per fortuna il TPL, con le comode parole chiave 'async/await', lo rende abbastanza semplice. Ho un metodo primitivo sulla simulazione in questo modo:Segnalazioni efficienti Compiti per il completamento di TPL su eventi ricorrenti di frequente
public Task CycleExecutedEvent()
{
lock (_cycleExecutedBroker)
{
if (!IsRunning) throw new TaskCanceledException("Simulation has been stopped");
return _cycleExecutedBroker.RegisterForCompletion(CycleExecutedEventName);
}
}
Questa è fondamentalmente la creazione di una nuova TaskCompletionSource e poi tornare un task. Lo scopo di questa attività è di eseguirne la continuazione quando si verifica il nuovo 'ExecuteCycle' nella simulazione.
Ho poi alcuni metodi di estensione come questo:
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());
}
Questi sono molto pratico, quindi, per la costruzione di sequenze da una prospettiva 'operatore', intraprendere azioni basate su condizioni e in attesa per periodi di tempo simulato. Il problema che sto incontrando è che CycleExecuted si verifica molto frequentemente (all'incirca ogni millisecondo se utilizzo a velocità completamente accelerata). Poiché questi metodi di attesa "wait" registrano una nuova "attesa" per ogni ciclo, ciò provoca un notevole turnover nelle istanze di TaskCompletionSource.
Ho profilato il mio codice e ho scoperto che circa il 5,5% del tempo totale della mia CPU viene speso all'interno di questi completamenti, di cui solo una percentuale trascurabile viene spesa nel codice 'attivo'. In pratica, tutto il tempo è dedicato alla registrazione di nuovi completamenti in attesa che le condizioni di attivazione siano valide.
La mia domanda: come posso migliorare le prestazioni qui pur mantenendo la comodità del async/await modello per la scrittura di 'comportamenti' operatore? Sto pensando che ho bisogno di qualcosa come un TaskCompletionSource più leggero e/o riutilizzabile, dato che l'evento di attivazione si verifica così frequentemente.
Ho fatto un po 'più di ricerca e suona come una buona opzione potrebbe essere quella di creare un'implementazione personalizzata del modello Awaitable, che potrebbe collegare direttamente l'evento, eliminando la necessità di un po' di TaskCompletionOrganizzazioni di risorse e attività. La ragione per cui potrebbe essere utile qui è che ci sono un sacco di continuazioni diverse in attesa di CycleExecutedEvent e devono essere attese frequentemente. Quindi, idealmente, sto cercando un modo per mettere in coda i callback di continuazione, quindi richiamare tutto in coda ogni volta che si verifica l'evento. Continuerò a scavare, ma accolgo con favore qualsiasi aiuto se la gente conosce un modo pulito per farlo.
Per chiunque che stanno guardando questa domanda in futuro, qui è la consuetudine awaiter ho messo insieme:
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;
}
}
E nel simulatore:
private readonly CycleExecutedAwaiter _cycleExecutedAwaiter = new CycleExecutedAwaiter();
public CycleExecutedAwaiter CycleExecutedEvent()
{
if (!IsRunning) throw new TaskCanceledException("Simulation has been stopped");
return _cycleExecutedAwaiter;
}
E 'un po' buffo, come il cameriere non segnala mai Completato, ma gli incendi continuano a chiamare i completamenti man mano che vengono registrati; ancora, funziona bene per questa applicazione. Ciò riduce il sovraccarico della CPU dal 5,5% al 2,1%. Probabilmente richiederà ancora qualche ritocco, ma è un bel miglioramento rispetto all'originale.
Ehi, non è giusto rispondere alla tua domanda un minuto prima di me! :-) – svick
@svick, non ha ancora risposto pienamente; Devo ancora capire come creare il cameriere personalizzato. :) Grazie per i link; quelli sono abbastanza utili. –
@DanBryant: dovresti rispondere alla tua domanda ... come risposta piuttosto che nel corpo della tua stessa domanda. – user7116