431

C#(.NET Async CTP)のawaitキーワードは、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の呼び出しは、(ほとんどの場合)無期限にブロックされ、他のスレッドがロックを取得しようとするときにデッドロックが発生するようです。私の回避策の信頼性の低さと、ロックステートメントで待機ステートメントが許可されない理由は何らかの形で関連していると思います。

ロックステートメントの本文内で待機が許可されない理由を誰かが知っていますか?

4

8 に答える 8

445

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

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

ExitDisposable.Dispose内でMonitor.Exitを呼び出すと、(ほとんどの場合)無期限にブロックされ、他のスレッドがロックを取得しようとするときにデッドロックが発生するようです。私の回避策の信頼性の低さと、ロックステートメントで待機ステートメントが許可されない理由は何らかの形で関連していると思います。

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

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

さらに悪いことに、コードは別のスレッドで再開する可能性があります(高度なシナリオでは、通常、待機を行ったスレッドで再度取得しますが、必ずしもそうとは限りません)。この場合、ロック解除は、取得したスレッドとは異なるスレッドでロックをロック解除します。ロックを外します。それはいい考えですか?いいえ。

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

于 2011-09-30T15:30:26.680 に答える
366

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

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
于 2013-08-15T16:22:18.153 に答える
77

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

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

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

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

したがって、基本的にここには2つの競合する要件があります。ここで最初の要件を実行しようとしてはいけません。2番目のアプローチを採用する場合は、await式で2つの分離されたロックブロックを分離することで、コードをより明確にすることができます。

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

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

于 2011-09-30T15:29:48.310 に答える
64

これは、この回答の単なる拡張です。

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

    // overloading variant for non-void methods with return type (generic T)
    public async Task<T> LockAsync<T>(Func<Task<T>> worker)
    {
        await _semaphore.WaitAsync();
        try
        {
            return await worker();
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

使用法:

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

    public async Task DoTest()
    {
        await _locker.LockAsync(async () =>
        {
            // [async] calls can be used within this block 
            // to handle a resource by one thread. 
        });
        // OR
        var result = await _locker.LockAsync(async () =>
        {
            // [async] calls can be used within this block 
            // to handle a resource by one thread. 
        });
    }
}
于 2018-05-02T16:47:09.693 に答える
18

これは、非同期調整プリミティブの構築、パート6:AsyncLockhttp ://winrtstoragehelper.codeplex.com/、Windows 8アプリストア、および.net4.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/の元のバージョンとStephenToubのAsyncLockを使用する変更されたバージョン テスト呼び出しを含むWindows8アプリストアアプリです。

このリンクも提案できますか:http: //www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

于 2012-12-26T02:58:26.947 に答える
9

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 Frameworkに組み込まれたメソッドが必要な場合は、SemaphoreSlim.WaitAsync代わりにを使用してください。リーダー/ライターロックは取得できませんが、実装の試行とテストは行われます。

于 2014-09-12T08:18:37.880 に答える
2

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

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();
        }
    }
}
于 2013-06-04T22:40:02.263 に答える
-1

動作しているように見えるが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; } }

于 2017-09-01T10:15:32.337 に答える