2013-01-22 8 views
10

In un'applicazione WPF, ho una classe che riceve i messaggi sulla rete. Ogni volta che un oggetto di detta classe 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.Come evitare la rientranza con i gestori di eventi asincroni vuoti?

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 elaborazione. Qual è il modo più semplice per garantire che i messaggi vengano applicati solo uno alla volta? Finora, mi è venuta in mente la 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<bool>(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, appare anche questo produrrà inevitabilmente una "perdita di memoria", perché il compito di applicare un messaggio sarò sempre prima attendere l'attività che ha applicato il messaggio precedente. Se è così, 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)); 
} 

E 'questo un modo ragionevole per evitare reentrancy con i gestori di eventi vuoto asincrone?

EDIT: Rimossa l'inutile dichiarazione await this.pending; in OnMessageReceived.

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

+0

@Servy: Vuoi dire in OnMessageReceived? Bella domanda, immagino che non sia necessario. –

+0

@Servy: Sono d'accordo che non è necessario in OnMessageReceived, ma è in ApplyToModel, giusto? –

+0

Vedo cosa stai facendo ora. – Servy

risposta

12

Qui dobbiamo ringraziare Stephen Toub, poiché ha alcuni utili costrutti di blocco asincrono dimostrati in una serie di blog, incluso un blocco async lock.

Ecco il codice da tale articolo (tra cui alcuni codice precedente articolo della serie):

public class AsyncLock 
{ 
    private readonly AsyncSemaphore m_semaphore; 
    private readonly Task<Releaser> m_releaser; 

    public AsyncLock() 
    { 
     m_semaphore = new AsyncSemaphore(1); 
     m_releaser = Task.FromResult(new Releaser(this)); 
    } 

    public Task<Releaser> 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<TaskCompletionSource<bool>> m_waiters = new Queue<TaskCompletionSource<bool>>(); 
    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<bool>(); 
       m_waiters.Enqueue(waiter); 
       return waiter.Task; 
      } 
     } 
    } 
    public void Release() 
    { 
     TaskCompletionSource<bool> toRelease = null; 
     lock (m_waiters) 
     { 
      if (m_waiters.Count > 0) 
       toRelease = m_waiters.Dequeue(); 
      else 
       ++m_currentCount; 
     } 
     if (toRelease != null) 
      toRelease.SetResult(true); 
    } 
} 

Ora applicarlo al vostro 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)); 
    } 
} 
+4

Altre opzioni includono il tipo ['SemaphoreSlim' in .NET 4.5] (http://msdn.microsoft.com/en-us/library/vstudio/system.threading.semaphoreslim (v = vs.100).aspx) e il tipo ['AsyncLock' nella mia libreria AsyncEx] (http://nitoasyncex.codeplex.com/wikipage?title=AsyncLock). –

+0

@StephenCleary La soluzione Semaphore non prevede un'attesa di blocco, non un'attesa asincrona? – Servy

+0

@Servy Non se si utilizza il metodo 'WaitAsync()'. – svick

1

Dato un EventHandler che utilizza Aspettiamo asincrona che non possiamo usare un blocco all'esterno dell'Attività perché il thread chiamante è lo stesso per ogni chiamata di evento, quindi il blocco 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 all'interno del compito, perché Task.Run genera sempre un nuovo compito che non viene eseguito 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); 
     } 
    }); 
} 

Così, quando un evento chiamate OnMessageReceived esso restituisce immidiatly e model.Apply è inserito solo uno dopo l'altro.

Problemi correlati