2013-01-22 2 views
10

In einer WPF-Anwendung habe ich eine Klasse, die Nachrichten über das Netzwerk empfängt. Immer wenn ein Objekt der Klasse eine vollständige Nachricht erhalten hat, wird ein Ereignis ausgelöst. Im MainWindow der Anwendung habe ich einen Event-Handler, der dieses Event abonniert hat. Der Event-Handler wird garantiert auf dem GUI-Thread der Anwendung aufgerufen.Wie vermeidet man die erneute Teilnahme mit asynchronen void Event-Handlern?

Wenn der Event-Handler aufgerufen wird, muss der Inhalt der Nachricht auf das Modell angewendet werden. Dies kann sehr kostspielig sein (> 200ms bei aktueller Hardware). Aus diesem Grund wird die Nachricht mit Task.Run auf den Thread-Pool ausgelagert.

Nun können Nachrichten in sehr kurzer Folge empfangen werden, sodass der Ereignishandler aufgerufen werden kann, während eine vorherige Änderung noch verarbeitet wird. Was ist der einfachste Weg, um sicherzustellen, dass Nachrichten nur einmal angewendet werden? Bisher habe ich mit folgendem kommen:

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. 
    } 
} 

Dies scheint wie erwartet zu funktionieren, aber es scheint auch dies wird unweigerlich ein „Gedächtnis Leck“ erzeugen, weil die Aufgabe eine Nachricht an gelten immer zuerst Warten Sie auf die Aufgabe, die die vorherige Nachricht angewendet hat. Wenn ja, dann sollte die folgende Änderung das Leck vermeiden:

private async Task ApplyToModel(EventArgs e) 
{ 
    if (!this.pending.IsCompleted) 
    { 
     await this.pending; 
    } 

    await Task.Run(() => this.model.Apply(e)); 
} 

Ist die eine sinnvolle Art und Weise reentrancy mit Asynchron Leeren Event-Handler zu vermeiden?

EDIT: Die unnötige Anweisung in OnMessageReceived entfernt.

EDIT 2: Die Nachrichten müssen in derselben Reihenfolge auf das Modell angewendet werden, in der sie empfangen wurden.

+0

@Servy: Sie meinen in OnMessageReceived? Gute Frage, ich denke, es ist nicht notwendig. –

+0

@Servy: Ich stimme zu, es ist nicht notwendig in OnMessageReceived, aber es ist in ApplyToModel, richtig? –

+0

Ich sehe was du damit machst. – Servy

Antwort

12

Wir müssen Stephen Toub hier danken, da er einige sehr nützliche asynchrone Sperrkonstrukte hat, die in einer Blogserie demonstriert wurden, einschließlich eines async lock Blocks. Hier

ist der Code aus diesem Artikel (darunter auch einige Code aus dem vorherigen Artikel in der 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); 
    } 
} 

Jetzt ist es zu Ihrem Fall Anwendung:

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

Andere Optionen umfassen den ['SemaphoreSlim' -Typ in .NET 4.5] (http://msdn.microsoft.com/en-us/library/vstudio/system.threading.semaphoreslim (v = vs.100).aspx) und der ['AsyncLock'-Typ in meiner AsyncEx-Bibliothek] (http://nitoasynx.codeplex.com/wikipage?title=AsyncLock). –

+0

@StephenCleary Wird die Semaphore-Lösung keine blockierende Wartezeit, keine asynchrone warten? – Servy

+0

@Servy Nicht, wenn Sie die 'WaitAsync()' Methode verwenden. – svick

1

einen Eventhandler gegeben, die verwendet async wartet darauf, dass wir keine Sperre außerhalb der Task verwenden können, weil der aufrufende Thread für jeden Ereignisaufruf derselbe ist, so dass die Sperre ihn immer passieren lässt.

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); 
} 

Aber wir können in der vorgegebenen Aufgaben sperren, weil Task.Run immer eine neue Aufgabe erzeugt, die auf dem gleichen Thread nicht

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); 
     } 
    }); 
} 

So parallel laufen, wenn ein Ereignis Anrufe es OnMessageReceived kehrt immidiatly und model.Apply wird nur nacheinander eingegeben.

Verwandte Themen