如何避免async void事件处理程序的重入
本文关键字:程序 事件处理 何避免 async void | 更新日期: 2023-09-27 17:55:04
在WPF应用程序中,我有一个通过网络接收消息的类。每当所述类的对象接收到完整消息时,就会引发一个事件。在应用程序的主窗口中,我有一个订阅该事件的事件处理程序。事件处理程序保证在应用程序的GUI线程上被调用。
无论何时调用事件处理程序,都需要将消息的内容应用于模型。这样做可能非常昂贵(在当前的硬件上>200ms)。这就是为什么使用Task.Run将应用消息卸载到线程池中。
现在,可以以非常接近的顺序接收消息,因此可以在处理前一个更改时调用事件处理程序。确保一次只应用一条消息的最简单方法是什么?到目前为止,我已经想到了以下内容:
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.
}
}
这似乎像预期的那样工作,但是它也似乎不可避免地会产生"内存泄漏",因为应用消息的任务总是首先等待应用前一个消息的任务。如果是这样,那么以下更改应该避免泄漏:
private async Task ApplyToModel(EventArgs e)
{
if (!this.pending.IsCompleted)
{
await this.pending;
}
await Task.Run(() => this.model.Apply(e));
}
这是一个明智的方式来避免重入与异步void事件处理程序?
EDIT:删除OnMessageReceived
中不必要的await this.pending;
语句。
EDIT 2:消息必须按照它们被接收的顺序应用到模型。
在这里我们要感谢Stephen Toub,因为他在一个博客系列中展示了一些非常有用的异步锁结构,包括一个异步锁块。
下面是那篇文章中的代码(包括本系列上一篇文章中的一些代码):
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);
}
}
现在把它应用到你的案例中:
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));
}
}
给定一个使用异步等待的事件处理程序,我们不能在任务外使用锁因为每个事件调用的调用线程都是相同的,所以锁总是让它通过。
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);
}
但是我们可以锁在Task里面,因为Task。Run总是生成一个新的Task,该Task不是在同一个线程上并行运行的
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);
}
});
}
所以当一个事件调用onmessagerreceived时,它会立即返回模型。Apply只能依次输入