ロックステートメントの本文内で 'await'演算子を使用できないのはなぜですか?


348

C#のawaitキーワード(.NET Async CTP)は、lockステートメント内からは使用できません。

MSDNから:

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

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

私は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ステートメントで許可されていない理由は、どういうわけか関係しています。

ロック文の本文内でawaitが許可されない理由を誰かが知っていますか?


27
それが許されない理由を見つけたと思います。
asawyer 2011

3
このリンクをお勧めします:hanselman.com/blog/…そしてこれは:blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx
hans

私は追いついて、非同期プログラミングについてもう少し学び始めています。私のwpfアプリケーションで多数のデッドロックが発生した後、この記事は非同期プログラミングの実践における優れた安全策であることがわかりました。msdn.microsoft.com/en-us/magazine/...
C. Tewalt

ロックは、非同期アクセスによってコードが壊れたときに非同期アクセスを防ぐように設計されています。ロック内で非同期を使用している場合はエルゴがロックを無効にしているため、ロック内で何かを待つ必要がある場合は、ロックを正しく使用していません
MikeT

回答:


366

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

いいえ、実装することは決して困難でも不可能でもありません。自分で実装したという事実は、その事実の証拠です。むしろ、それは信じられないほど悪い考えであり、あなたがこの過ちを犯すことからあなたを守るために、私たちはそれを許しません。

ExitDisposable.Dispose内のMonitor.Exitへの呼び出しは、他のスレッドがロックを取得しようとするため、無期限に(ほとんどの場合)ブロックしてデッドロックを引き起こしているようです。私の作業の信頼性が低いことと、awaitステートメントがlockステートメントで許可されていない理由は、どういうわけか関係しています。

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

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

さらに悪いことに、コードは別のスレッドで再開できます(高度なシナリオでは、通常、待機を行ったスレッドで再びピックアップしますが、必ずしもそうではありません)。ロックアウト。それは良い考えですか?番号。

同じ理由で、yield return内部でを実行することも「最悪の慣行」であることに注意しlockます。これは合法ですが、違法にしておけばよかったのですが。「待つ」と同じ間違いをするつもりはありません。


189
キャッシュエントリを返す必要があるシナリオをどのように処理しますか?エントリが存在しない場合、コンテンツを非同期に計算する必要があり、その後、エントリを追加+返します。
Softlion、2012

9
私はここのパーティーに遅れていることを知っていますが、これが悪い考えである主な理由としてデッドロックを設定していることに驚いたのです。ロック/モニターのリエントラントな性質が問題の大きな部分になるだろうと私は自分の考えで結論に達しました。つまり、2つのタスクをスレッドプールにキューイングして、lock()を実行します。これは、同期の世界では別のスレッドで実行されます。しかし、await(許可されている場合)を使用すると、スレッドが再利用されたため、ロックブロック内で2つのタスクを実行できます。陽気さが続く。または私は何かを誤解しましたか?
Gareth Wilson、

4
@GarethWilson:デッドロックについて質問されたので、デッドロックについて話しました。奇妙な再入可能性の問題が発生する可能性があり、その可能性が高いように思われます。
Eric Lippert

11
@Eric Lippert。SemaphoreSlim.WaitAsyncこの回答を投稿した後、クラスが.NETフレームワークに追加されたことを考えると、今はそれが可能であると考えることができます。これに関係なく、そのような構成を実装することの難しさについてのあなたのコメントはまだ完全に有効です。
コンタンゴ

7
「awaitが呼び出し元に制御を返し、メソッドが再開するまでの間に任意のコードが実行されます」-マルチスレッドのコンテキストでは、非同期/待機がない場合でも、これはどのコードにも当てはまります。他のスレッドが任意のコードを実行する可能性があります時間、そしてあなたが言うように任意のコードは言った「ロック順序逆転を生成するロックを取り出しているかもしれない、それ故にデッドロック」。では、なぜこれが非同期/待機で特に重要なのでしょうか。「コードが別のスレッドで再開できる」という2番目のポイントは、非同期/待機にとって特に重要であることを理解しています。
bacar

291

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

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

10
この方法が最近.NETフレームワークに導入されたので、非同期/待機の世界でのロックの概念は今では十分に実証されていると思います。
Contango

5
詳細については、この記事で「SemaphoreSlim」というテキストを検索してください。非同期/
待機

1
@JamesKoすべてのタスクが結果を待っている場合、Stuff私はそれを回避する方法が見当たらない...
オハドシュナイダー

7
mySemaphoreSlim = new SemaphoreSlim(1, 1)ように動作するように初期化されるべきではありませんlock(...)か?
セルゲイ

3
この回答の拡張バージョンを追加しました:stackoverflow.com/a/50139704/1844247
Sergey

67

基本的にそれを行うのは間違っています。

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

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

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

だから、基本的には、ここで2つの競合する要件があります-あなたはすべきではありませんしようと最初にここで行うには、あなたが第二のアプローチを取りたい場合は、2つの別々のロック・ブロックがのawait式で分離させることによって、コードをより明確にすることができます。

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

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


5
SemaphoreSlim.WaitAsyncこの回答を投稿した後、クラスが.NETフレームワークに追加されたことを考えると、今はそれが可能であると考えることができます。これに関係なく、そのような構成を実装することの難しさについてのあなたのコメントはまだ完全に有効です。
Contango 2014

7
@Contango:それはまったく同じことではありません。特に、セマフォは特定のスレッドに関連付けられていません。ロックと同様の目標を達成しますが、大きな違いがあります。
Jon Skeet 2014

@JonSkeet私はこれが非常に古いスレッドであり、すべてであることを知っていますが、something()コールが2番目の方法でこれらのロックを使用してどのように保護されているかわかりませんか?スレッドがsomething()を実行しているとき、他のスレッドもそれに関与する可能性があります。ここで何か不足していますか?

@ジョセフ:それはその時点では保護されていません。これは2番目のアプローチであり、取得または解放してから、おそらく別のスレッドで取得または解放していることを明確にします。エリックの答えによると、最初のアプローチは悪い考えです。
Jon Skeet、

41

これはこの回答の拡張にすぎません。

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

1
tryブロックの外でセマフォロックを取得するのは危険な場合があります。その間WaitAsyncで例外が発生しtry、セマフォが解放されない場合(デッドロック)。一方、WaitAsync呼び出しをtryブロックに移動すると、ロックを取得せずにセマフォを解放できるという別の問題が発生します。:この問題を説明した関連スレッドを参照してくださいstackoverflow.com/a/61806749/7889645
AndreyCh

16

このreferes http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspxhttp://winrtstoragehelper.codeplex.com/ 、Windows 8のアプリストアと.NET 4.5

これは私のこれの角度です:

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

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

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

ただし、ユーティリティの最初のファイルを開く操作(保存機能内)も非同期であるため、最初の待機はフレームワークに制御を返し、いつかは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つのロックでユーティリティのすべてのファイル操作をロックします。これは不必要に強力ですが、私のシナリオでは問題なく機能します。

ここ から元のバージョンのためのいくつかのテスト・コールを使用してWindows 8アプリストアアプリ:私のテストプロジェクトですhttp://winrtstoragehelper.codeplex.com/スティーブンToubからAsyncLock使用していますし、私の修正版//blogs.msdn:HTTPが。 com / b / pfxteam / archive / 2012/02/12 / 10266988.aspx

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


7

Stephen Taubがこの質問に対するソリューションを実装しました。非同期調整プリミティブの構築、パート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代わりにを使用してください。リーダー/ライターのロックは取得できませんが、実装を試し、テストします。


このコードを使用する際の注意点があるかどうか知りたいです。誰かがこのコードで問題を実証できるなら、私は知りたいのですが。ただし、SemaphoreSlim.WaitAsync.NETフレームワークと同様に、非同期/待機ロックの概念は確かに十分に実証されています。このコードが行うのは、リーダー/ライターロックの概念を追加することだけです。
Contango 2014

3

うーん、醜く見えますが、うまくいくようです。

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

0

動作しているように見えますが、GOTCHAがあるモニター(以下のコード)を使用しようとしました...複数のスレッドがある場合、それにより... 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; } }

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.