簡體   English   中英

如何避免使用異步void事件處理程序重入?

[英]How to avoid reentrancy with async void event handlers?

在WPF應用程序中,我有一個通過網絡接收消息的類。 每當所述類的對象收到完整消息時,就會引發一個事件。 在應用程序的MainWindow中,我有一個訂閱該事件的事件處理程序。 保證在應用程序的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事件處理程序重入的一種明智方法嗎?

編輯 :刪除了不必要的await this.pending; OnMessageReceived語句。

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

給定一個使用異步等待的事件處理程序,我們不能在Task外部使用鎖,因為調用線程對於每個事件調用都是相同的,因此鎖將始終讓它通過。

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,它不會在同一個線程上並行運行

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

因此,當一個事件調用OnMessageReceived時,它會返回immidiatly和model.Apply,只能一個接一個地輸入。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM