13

WPF アプリケーションには、ネットワーク経由でメッセージを受信するクラスがあります。上記のクラスのオブジェクトが完全なメッセージを受信するたびに、イベントが発生します。アプリケーションの MainWindow には、そのイベントにサブスクライブされたイベント ハンドラーがあります。イベント ハンドラーは、アプリケーションの GUI スレッドで呼び出されることが保証されています。

イベント ハンドラーが呼び出されるたびに、メッセージの内容をモデルに適用する必要があります。これを行うと、非常にコストがかかる可能性があります (現在のハードウェアでは 200 ミリ秒以上)。そのため、メッセージの適用は、Task.Run を使用してスレッド プールにオフロードされます。

現在、メッセージは非常に近い間隔で受信できるため、前の変更がまだ処理されている間にイベント ハンドラーを呼び出すことができます。メッセージが一度に 1 つだけ適用されるようにする最も簡単な方法は何ですか? これまでのところ、次のことを思いつきました。

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

これは、async void イベント ハンドラーで再入可能性を回避する賢明な方法ですか?

編集: の不要なawait this.pending;ステートメントを削除しましたOnMessageReceived

EDIT 2 : メッセージは、受信されたのと同じ順序でモデルに適用する必要があります。

4

2 に答える 2

12

ここで、Stephen Toub に感謝する必要があります。彼は、async lockブロックを含む、いくつかの非常に便利な非同期ロック構造をブログ シリーズで紹介しています。

その記事のコードを次に示します (シリーズの前の記事のコードをいくつか含む)。

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));
    }
}
于 2013-01-22T17:36:42.677 に答える
1

async await を使用するイベント ハンドラーの場合、タスクの外部でロックを使用することはできません。これは、呼び出し元のスレッドがすべてのイベント呼び出しで同じであるため、ロックは常にそれを通過させるためです。

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

そのため、イベントが OnMessageReceived を呼び出すと、すぐに返され、model.Apply が次々と入力されるだけです。

于 2014-04-04T13:31:51.737 に答える