非同期の同期

awaitを使った時のロックって、どうやるんですかね。セマフォでいいの?

C#では、バージョン5.0から、async/awaitキーワードを用いることで、非同期処理がとてつもなく簡単に書けるようになりました。
今までは、時間のかかる処理は新しいスレッドを作ったり、Taskで実行したり、IO処理はBegin/Endパターンとか、EAP使ったりしました。うんざりしますね!まじで!!

今では、async/awaitが導入されたことにより、ほぼ同期的なコードと同じコードを書いてるにもかかわらず、時間のかかる処理やIO処理を非同期に行えるようになりました。

さて、同期的にコードが書けてしまうと、おもわず非同期部分をlockしたくなってしまいます。
たとえばファイルの書き込みとか。非同期にしたいけど、書き込み自体は排他的にしてほしい。

lock(lockObj)
{
    using (var stream = File.OpenWrite(path))
    {
         await stream.WriteLineAsync("A");
         await stream.WriteLineAsync("B");
    }
}

みたいな。でもこれはアウトです。
lockの実態はMonitorですが、これはあるスレッドでロックして、別スレッドで返すことはできません。
await前後は同じスレッドで実行されるとは限らないため、lock内にawaitを入れるとエラーが出るわけです。

で…
どうすればいいの?これ。
とりあえず私は、SemaphoreSlimを使っています。普通のSemaphoreでもいいんですが、Slimのほうにはシグナルを待つためにWaitAsyncというTask<bool>を返すメソッドがあって、非同期処理と相性が良いようにも思えます。こんな感じ↓

private SemaphoreSlim _semaphore = new SemaphoreSlim(1);
private async Task FunctionAsync()
{
    try
    {
        await _semaphore.WaitAsync();
        await 非同期だけど同時に実行してほしくない処理();
    }
    finally
    {
        _semaphore.Release();
    }
}

で、毎回try/finally書くのはめんどくさいんで、こんな↓クラス作って

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ShComp
{
    /// <summary>
    /// セマフォを用いた、非同期の状態でもロックを行うためのメソッドを提供します。
    /// </summary>
    class AsyncLock
    {
        private SemaphoreSlim _s;

        public AsyncLock()
        {
            _s = new SemaphoreSlim(1);
        }

        /// <summary>
        /// <see cref="System.Threading.SemaphoreSlim" /> に移行するために非同期に待機します。<para />
        /// 待機後、IDisposable型のオブジェクトを返します。
        /// 返されるオブジェクトをDisposeすることで <see cref="System.Threading.SemaphoreSlim" /> から出ます。
        /// usingと共に使うことを想定しています。
        /// </summary>
        public async Task<IDisposable> GetObjectAsync()
        {
            await _s.WaitAsync();
            return new _(_s);
        }

        private class _ : IDisposable
        {
            private SemaphoreSlim _s;

            public _(SemaphoreSlim s)
            {
                _s = s;
            }

            public void Dispose()
            {
                _s.Release();
            }
        }
    }
}

こんな感じ↓で使っています。 …いいのかな?

private AsyncLock _lock = new AsyncLock();
private async Task FunctionAsync()
{
    using (await _lock.GetObjectAsync())
    {
        await 非同期だけど同時に実行してほしくない処理();
    }
}
カテゴリー: プログラミング タグ: , パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です