web-dev-qa-db-ja.com

ロック文の本文内で「await」演算子を使用できないのはなぜですか?

C#(.NET非同期CTP)のawaitキーワードは、ロックステートメント内から許可されていません。

MSDN から:

await式は使用できません同期関数、クエリ式、例外処理ステートメントのcatchまたはfinallyブロック、ロックステートメントのブロック内、または安全でないコンテキスト内。

コンパイラーチームが何らかの理由でこれを実装することは困難または不可能だと思います。

Usingステートメントを使用して回避策を試みました。

class Async
{
    public static async Task<IDisposable> Lock(object obj)
    {
        while (!Monitor.TryEnter(obj))
            await TaskEx.Yield();

        return new ExitDisposable(obj);
    }

    private class ExitDisposable : IDisposable
    {
        private readonly object obj;
        public ExitDisposable(object obj) { this.obj = obj; }
        public void Dispose() { Monitor.Exit(this.obj); }
    }
}

// example usage
using (await Async.Lock(padlock))
{
    await SomethingAsync();
}

ただし、これは期待どおりに機能しません。 ExitDisposable.Dispose内のMonitor.Exitの呼び出しは、他のスレッドがロックを取得しようとする際にデッドロックを引き起こす(ほとんどの場合)無期限にブロックするようです。私は回避策の信頼性が低いと思うし、await文がlock文で許可されない理由はどういうわけか関連しています。

why awaitがlockステートメントの本文内で許可されていないことを知っていますか?

294
Kevin

コンパイラーチームが何らかの理由でこれを実装することは困難または不可能だと思います。

いいえ、実装することはまったく困難でも不可能でもありません。自分で実装したという事実は、その事実の証です。むしろ、それは信じられないほど悪い考えですですので、この間違いを防ぐために許可していません。

exitDisposable.Dispose内でMonitor.Exitを呼び出すと、他のスレッドがロックを取得しようとするため、無期限に(ほとんどの場合)ブロックされ、デッドロックが発生するようです。私は回避策の信頼性が低いと思うし、await文がlock文で許可されない理由はどういうわけか関連しています。

正しい、あなたは私たちがそれを違法にした理由を発見した。 ロック内で待機することは、デッドロックを生成するためのレシピです。

理由がわかると思います:awaitが呼び出し元に制御を返した後、メソッドが再開するまでの間に任意のコードが実行されるその任意のコードは、ロックの順序の反転、つまりデッドロックを引き起こすロックを取得している可能性があります。

さらに悪いことに、コードは別のスレッドで再開する可能性がありますこの場合、ロックを解除したスレッドとは異なるスレッドでロックを解除します。それは良い考えですか?いや.

同じ理由で、lock内でyield returnを実行することも「最悪のプラクティス」であることに注意してください。そうすることは合法ですが、私たちがそれを違法にしたことを望みます。 「待ち」についても同じ間違いをするつもりはありません。

321
Eric Lippert

SemaphoreSlim.WaitAsync メソッドを使用します。

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
238
user1639030

基本的には間違ったことです。

このcouldを実装する方法は2つあります。

  • ブロックの最後でのみロックを解除し、ロックを保持する
    非同期操作の所要時間がわからないため、これは非常に悪い考えです。 minimalの時間だけロックを保持する必要があります。また、threadがメソッドではなくロックを所有しているため、同じスレッド上で非同期メソッドの残りを実行することもできないため、潜在的に不可能です。 (タスクスケジューラに依存)。

  • 待機中のロックを解除し、待機が戻ったときに再取得する
    これは、非同期メソッドが同等の同期コードのように可能な限り厳密に動作する必要がある、最小限の驚きIMOの原則に違反します。ロックブロックでMonitor.Waitを使用しない限り、ブロック。

したがって、基本的にここには2つの競合する要件があります。ここで最初にを試してしてはいけません。 await式で区切られた2つの独立したロックブロックを使用して、コードをより明確にします。

// Now it's clear where the locks will be acquired and released
lock (foo)
{
}
var result = await something;
lock (foo)
{
}

したがって、ロックブロック自体で待機することを禁止することにより、言語はあなたが本当にやりたいことを考えさせ、その選択をより明確にしますあなたが書くコードで。

64
Jon Skeet

これは この答えの単なる拡張です

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

public class SemaphoreLocker
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task LockAsync(Func<Task> worker)
    {
        await _semaphore.WaitAsync();
        try
        {
            await worker();
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

使用法:

public class Test
{
    private static readonly SemaphoreLocker _locker = new SemaphoreLocker();

    public async Task DoTest()
    {
        await _locker.LockAsync(async () =>
        {
            // [asyn] calls can be used within this block 
            // to handle a resource by one thread. 
        });
    }
}
19
Sergey

これは http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspxhttp://winrtstoragehelper.codeplex.com/ 、Windows 8アプリストアおよび.net 4.5

これに関する私の角度は次のとおりです。

Async/await言語機能により、多くのことが非常に簡単になりますが、非同期呼び出しを使用するのが非常に簡単になる前にはめったに遭遇しなかったシナリオ、つまり再入可能性も導入されます。

これは特にイベントハンドラーに当てはまります。これは、多くのイベントでは、イベントハンドラーから戻った後に何が起こっているのかについて何の手掛かりもないためです。実際に発生する可能性のあることの1つは、最初のイベントハンドラーで待機している非同期メソッドが、同じスレッド上にある別のイベントハンドラーから呼び出されることです。

Windows 8 App Storeアプリで出会った実際のシナリオは次のとおりです。私のアプリには2つのフレームがあります。フレームに出入りするデータをファイル/ストレージにロード/セーフにしたいです。 OnNavigatedTo/Fromイベントは、保存と読み込みに使用されます。保存と読み込みは、いくつかの非同期ユーティリティ関数( http://winrtstoragehelper.codeplex.com/ など)によって行われます。フレーム1からフレーム2に、または他の方向にナビゲートすると、非同期ロードおよび安全な操作が呼び出され、待機されます。イベントハンドラーは非同期になり、void =>待機できなくなります。

ただし、ユーティリティの最初のファイルを開く操作(保存機能内で言う)も非同期であるため、最初のawaitはフレームワークに制御を返し、後で2番目のイベントハンドラーを介して他のユーティリティ(ロード)を呼び出します。ロードは同じファイルを開こうとしますが、ファイルが保存操作のために今までに開いている場合、ACCESSDENIED例外で失敗します。

私にとっての最小の解決策は、usingとAsyncLockを使用してファイルアクセスを保護することです。

private static readonly AsyncLock m_lock = new AsyncLock();
...

using (await m_lock.LockAsync())
{
    file = await folder.GetFileAsync(fileName);
    IRandomAccessStream readStream = await file.OpenAsync(FileAccessMode.Read);
    using (Stream inStream = Task.Run(() => readStream.AsStreamForRead()).Result)
    {
        return (T)serializer.Deserialize(inStream);
    }
}

彼のロックは基本的に1つのロックでユーティリティのすべてのファイル操作をロックダウンすることに注意してください。これは不必要に強力ですが、私のシナリオでは正常に機能します。

ここ は私のテストプロジェクトです。 http://winrtstoragehelper.codeplex.com/ からの元のバージョンのテスト呼び出しがいくつかあるWindows 8アプリストアアプリと、 Stephen ToubのAsyncLockを使用します http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx

このリンクもお勧めします: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

15
hans

Stephen Taubは、この質問に対する解決策を実装しました。 Building Async Coordination Primitives、Part 7:AsyncReaderWriterLock を参照してください。

スティーブンタウブは業界で高く評価されているため、彼が書いたものはすべて堅実なものになりそうです。

彼がブログに投稿したコードは再現しませんが、使用方法を示します。

/// <summary>
///     Demo class for reader/writer lock that supports async/await.
///     For source, see Stephen Taub's brilliant article, "Building Async Coordination
///     Primitives, Part 7: AsyncReaderWriterLock".
/// </summary>
public class AsyncReaderWriterLockDemo
{
    private readonly IAsyncReaderWriterLock _lock = new AsyncReaderWriterLock(); 

    public async void DemoCode()
    {           
        using(var releaser = await _lock.ReaderLockAsync()) 
        { 
            // Insert reads here.
            // Multiple readers can access the lock simultaneously.
        }

        using (var releaser = await _lock.WriterLockAsync())
        {
            // Insert writes here.
            // If a writer is in progress, then readers are blocked.
        }
    }
}

メソッドが.NETフレームワークに組み込まれている場合は、代わりにSemaphoreSlim.WaitAsyncを使用します。リーダー/ライターのロックは取得しませんが、実装をテストしてテストします。

6
Contango

うーん、looksいように見えますが、動作しているようです。

static class Async
{
    public static Task<IDisposable> Lock(object obj)
    {
        return TaskEx.Run(() =>
            {
                var resetEvent = ResetEventFor(obj);

                resetEvent.WaitOne();
                resetEvent.Reset();

                return new ExitDisposable(obj) as IDisposable;
            });
    }

    private static readonly IDictionary<object, WeakReference> ResetEventMap =
        new Dictionary<object, WeakReference>();

    private static ManualResetEvent ResetEventFor(object @lock)
    {
        if (!ResetEventMap.ContainsKey(@lock) ||
            !ResetEventMap[@lock].IsAlive)
        {
            ResetEventMap[@lock] =
                new WeakReference(new ManualResetEvent(true));
        }

        return ResetEventMap[@lock].Target as ManualResetEvent;
    }

    private static void CleanUp()
    {
        ResetEventMap.Where(kv => !kv.Value.IsAlive)
                     .ToList()
                     .ForEach(kv => ResetEventMap.Remove(kv));
    }

    private class ExitDisposable : IDisposable
    {
        private readonly object _lock;

        public ExitDisposable(object @lock)
        {
            _lock = @lock;
        }

        public void Dispose()
        {
            ResetEventFor(_lock).Set();
        }

        ~ExitDisposable()
        {
            CleanUp();
        }
    }
}
4
Anton Pogonets

System.Threading.SynchronizationLockException同期化されていないコードブロックからオブジェクト同期メソッドが呼び出されました...

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

namespace MyNamespace
{
    public class ThreadsafeFooModifier : 
    {
        private readonly object _lockObject;

        public async Task<FooResponse> ModifyFooAsync()
        {
            FooResponse result;
            Monitor.Enter(_lockObject);
            try
            {
                result = await SomeFunctionToModifyFooAsync();
            }
            finally
            {
                Monitor.Exit(_lockObject);
            }
            return result;
        }
    }
}

これに先立ち、私は単純にこれを行っていましたが、ASP.NETコントローラーにあったため、デッドロックが発生しました。

public async Task<FooResponse> ModifyFooAsync() { lock(lockObject) { return SomeFunctionToModifyFooAsync.Result; } }

1
andrew pate