如何避免使用异步 void 事件处理程序重入? [英] How to avoid reentrancy with async void event handlers?

查看:22
本文介绍了如何避免使用异步 void 事件处理程序重入?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 WPF 应用程序中,我有一个通过网络接收消息的类.每当所述类的对象收到完整消息时,就会引发一个事件.在应用程序的 MainWindow 中,我有一个订阅该事件的事件处理程序.保证在应用程序的 GUI 线程上调用事件处理程序.

In a WPF application, I have a class that receives messages over the network. Whenever an object of said class has received a full message, an event is raised. In the MainWindow of the application I have an event handler subscribed to that event. The event handler is guaranteed to be called on the GUI thread of the application.

每当调用事件处理程序时,都需要将消息的内容应用到模型中.这样做可能非常昂贵(在当前硬件上 >200 毫秒).这就是使用 Task.Run 将应用消息卸载到线程池的原因.

Whenever the event handler is called, the contents of the message needs to be applied to the model. Doing so can be quite costly (>200ms on current hardware). That's why applying the message is offloaded onto the thread pool with Task.Run.

现在,可以非常接近地连续接收消息,因此可以在处理之前的更改时调用事件处理程序.确保一次只应用一条消息的最简单方法是什么?到目前为止,我想出了以下几点:

Now, messages can be received in very close succession, so the event handler can be called while a previous change is still being processed. What is the simplest way to ensure that messages are only applied one at time? So far, I've come up with the following:

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

这似乎按预期工作,但似乎这也将不可避免地产生内存泄漏",因为应用消息的任务将始终首先等待应用前一条消息的任务.如果是这样,那么以下更改应该可以避免泄漏:

This seems to work as expected, however it also appears this will inevitably produce a "memory leak", because the task to apply a message will always first wait on the task that applied the previous message. If so, then the following change should avoid the leak:

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

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

这是避免异步 void 事件处理程序重入的明智方法吗?

Is this a sensible way to avoid reentrancy with async void event handlers?

EDIT:删除了 OnMessageReceived 中不必要的 await this.pending; 语句.

EDIT: Removed the unnecessary await this.pending; statement in OnMessageReceived.

EDIT 2:必须按照接收消息的相同顺序将消息应用于模型.

EDIT 2: The messages must be applied to the model in the same order in which they have been received.

推荐答案

在这里我们要感谢 Stephen Toub,因为他在博客系列中展示了一些非常有用的异步锁定结构,包括一个 异步锁块.

We need to thank Stephen Toub here, as he has some very useful async locking constructs demonstrated in a blog series, including an async lock block.

这是那篇文章中的代码(包括本系列上一篇文章中的一些代码):

Here is the code from that article (including some code from the previous article in the series):

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

现在将其应用于您的案例:

Now applying it to your case:

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

这篇关于如何避免使用异步 void 事件处理程序重入?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆