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

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

问题描述

在一个WPF应用程序,我有在网络上接收消息的类。无论何时说类的一个对象已经接收一个完整的消息,引发事件。在应用程序的主窗口我有订阅了该事件的事件处理程序。事件处理程序是保证被称为应用程序的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.

现在,信息可以在非常接近陆续收到,因此而previous变化仍在处理的事件处理程序可以调用。这是确保消息只应用一个时间的最简单的方法是什么?到目前为止,我已经想出了以下内容:

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

这似乎工作如预期,但它也出现,这将不可避免地产生内存泄漏,因为任务应用的消息总是会先等待上所施加的previous消息的任务。如果是这样,那么下面的变化应避免泄漏:

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

这是一个明智的方式,以避免重入与异步无效的事件处理程序?

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

修改:删除不必要的计谋this.pending; 语句 OnMessageReceived

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

修改2 :这些消息必须被应用到模型中,他们已收到相同的顺序

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

推荐答案

我们需要感谢斯蒂芬Toub在这里,因为他有一些异步的博客系列展示了锁定结构,其中包括的async锁定块。

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.

下面是从那篇文章(包括该系列中的previous文章约$ C $三)code:

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

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

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