Come evitare la rientranza con i gestori di eventi asincroni vuoti?

In un’applicazione WPF, ho una class che riceve i messaggi sulla rete. Ogni volta che un object di detta class ha ricevuto un messaggio completo, viene generato un evento. Nel MainWindow dell’applicazione ho un gestore di eventi sottoscritto a quell’evento. Il gestore di eventi è garantito per essere chiamato sul thread GUI dell’applicazione.

Ogni volta che viene chiamato il gestore eventi, il contenuto del messaggio deve essere applicato al modello. Fare ciò può essere piuttosto costoso (> 200 ms sull’hardware attuale). Ecco perché l’applicazione del messaggio viene scaricata nel pool di thread con Task.Run.

Ora i messaggi possono essere ricevuti in successione molto ravvicinata, quindi il gestore eventi può essere chiamato mentre una modifica precedente è ancora in fase di elaborazione. Qual è il modo più semplice per garantire che i messaggi vengano applicati solo uno alla volta? Finora, ho trovato il seguente:

using System; using System.Threading.Tasks; using System.Windows; public partial class MainWindow : Window { private Model model = new Model(); private Task pending = Task.FromResult(false); // Assume e carries a message received over the network. private void OnMessageReceived(object sender, EventArgs e) { this.pending = ApplyToModel(e); } private async Task ApplyToModel(EventArgs e) { await this.pending; await Task.Run(() => this.model.Apply(e)); // Assume this is an expensive call. } } 

Questo sembra funzionare come previsto, tuttavia sembra anche che questo produrrà inevitabilmente una “perdita di memoria”, perché l’attività di applicare un messaggio attenderà sempre prima l’attività che ha applicato il messaggio precedente. In tal caso, la seguente modifica dovrebbe evitare la perdita:

 private async Task ApplyToModel(EventArgs e) { if (!this.pending.IsCompleted) { await this.pending; } await Task.Run(() => this.model.Apply(e)); } 

È un modo ragionevole per evitare la ricorrenza con gestori di eventi asincroni vuoti?

EDIT : Rimosso il non necessario await this.pending; dichiarazione in OnMessageReceived .

MODIFICA 2 : i messaggi devono essere applicati al modello nello stesso ordine in cui sono stati ricevuti.

Dobbiamo ringraziare Stephen Toub qui, poiché ha alcuni utili costrutti di blocco asincrono dimostrati in una serie di blog, incluso un blocco di blocco asincrono .

Ecco il codice di quell’articolo (incluso il codice dell’articolo precedente della serie):

 public class AsyncLock { private readonly AsyncSemaphore m_semaphore; private readonly Task m_releaser; public AsyncLock() { m_semaphore = new AsyncSemaphore(1); m_releaser = Task.FromResult(new Releaser(this)); } public Task LockAsync() { var wait = m_semaphore.WaitAsync(); return wait.IsCompleted ? m_releaser : wait.ContinueWith((_, state) => new Releaser((AsyncLock)state), this, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } public struct Releaser : IDisposable { private readonly AsyncLock m_toRelease; internal Releaser(AsyncLock toRelease) { m_toRelease = toRelease; } public void Dispose() { if (m_toRelease != null) m_toRelease.m_semaphore.Release(); } } } public class AsyncSemaphore { private readonly static Task s_completed = Task.FromResult(true); private readonly Queue> m_waiters = new Queue>(); private int m_currentCount; public AsyncSemaphore(int initialCount) { if (initialCount < 0) throw new ArgumentOutOfRangeException("initialCount"); m_currentCount = initialCount; } public Task WaitAsync() { lock (m_waiters) { if (m_currentCount > 0) { --m_currentCount; return s_completed; } else { var waiter = new TaskCompletionSource(); m_waiters.Enqueue(waiter); return waiter.Task; } } } public void Release() { TaskCompletionSource toRelease = null; lock (m_waiters) { if (m_waiters.Count > 0) toRelease = m_waiters.Dequeue(); else ++m_currentCount; } if (toRelease != null) toRelease.SetResult(true); } } 

Ora applicandolo al tuo caso:

 private readonly AsyncLock m_lock = new AsyncLock(); private async void OnMessageReceived(object sender, EventArgs e) { using(var releaser = await m_lock.LockAsync()) { await Task.Run(() => this.model.Apply(e)); } } 

Dato un eventhandler che usa async attendono che non possiamo usare un lock all’esterno dell’attività perché il thread chiamante è lo stesso per ogni chiamata di evento, quindi il lock lo lascerà sempre passare.

 var object m_LockObject = new Object(); private async void OnMessageReceived(object sender, EventArgs e) { // Does not work Monitor.Enter(m_LockObject); await Task.Run(() => this.model.Apply(e)); Monitor.Exit(m_LockObject); } 

Ma possiamo bloccare l’attività perché Task.Run genera sempre una nuova attività che non viene eseguita in parallelo sullo stesso thread

 var object m_LockObject = new Object(); private async void OnMessageReceived(object sender, EventArgs e) { await Task.Run(() => { // Does work lock(m_LockObject) { this.model.Apply(e); } }); } 

Quindi, quando un evento chiama OnMessageReceived, ritorna immidiatamente e model.Apply viene inserito solo uno dopo l’altro.