[C#] lock 문의 본문 내에서 ‘await’연산자를 사용할 수없는 이유는 무엇입니까?

C # (. NET Async CTP)의 await 키워드는 잠금 명령문 내에서 허용되지 않습니다.

에서 MSDN :

대기 표현식은 동기식 함수, 쿼리 표현식, 예외 처리 명령문의 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에 대한 호출은 다른 스레드가 잠금을 획득하려고 시도 할 때 교착 상태를 일으키는 무한정 (대부분) 차단되는 것으로 보입니다. 나는 내 작업의 신뢰성이 의심스럽고 잠금 진술에서 대기 진술이 허용되지 않는 이유는 어떻게 든 관련이 있다고 생각합니다.

누구든지 잠금 문의 본문 내에서 대기가 허용되지 않는 이유를 알고 있습니까?



답변

컴파일러 팀이 어떤 이유로 구현하기가 어렵거나 불가능하다고 가정합니다.

아니요, 구현하기가 전혀 어렵거나 불가능하지는 않습니다. 직접 구현했다는 사실은 그 사실에 대한 증거입니다. 오히려, 그것은 매우 나쁜 생각 이므로 우리는 당신 이이 실수를하지 않도록 보호하기 위해 그것을 허용하지 않습니다.

ExitDisposable.Dispose 내에서 Monitor.Exit를 호출하면 다른 스레드가 잠금을 획득하려고 시도 할 때 교착 상태를 일으키는 무한정 (대부분) 차단되는 것으로 보입니다. 나는 내 작업의 신뢰성이 의심스럽고 잠금 진술에서 대기 진술이 허용되지 않는 이유는 어떻게 든 관련이 있다고 생각합니다.

맞습니다, 왜 우리가 불법으로 만들 었는지 발견했습니다. 교착 상태를 생성하기위한 레시피는 잠금 내부를 기다리는 것입니다.

이유를 알 수 있습니다 : await는 호출자가 제어를 호출자에게 반환하고 메소드가 재개되는 시간 사이에 임의의 코드가 실행됩니다 . 이 임의의 코드는 잠금 순서 반전을 생성하는 잠금을 수행하므로 교착 상태가 발생할 수 있습니다.

더 나쁜 것은 코드가 다른 스레드 에서 재개 될 수 있다는 것입니다 (고급 시나리오에서는 일반적으로 대기중인 스레드에서 다시 선택하지만 반드시 그런 것은 아닙니다).이 경우 잠금 해제는 걸린 스레드와 다른 스레드에서 잠금을 해제합니다 자물쇠 밖으로. 좋은 생각입니까? 아니.

나는 같은 이유로 yield return내부 를 수행하는 것이 “가장 최악의 방법”이라는 것에 주목한다 lock. 그렇게하는 것은 합법적이지만 우리가 불법으로 만들었기를 바랍니다. 우리는 “대기”에 대해 같은 실수를하지 않을 것입니다.


답변

SemaphoreSlim.WaitAsync방법을 사용하십시오 .

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


답변

기본적으로 그것은 잘못된 일입니다.

이를 구현할 있는 두 가지 방법이 있습니다.

  • 잠금 장치를 잡고 블록 끝에서만 잠금을 해제하십시오 .
    비동기 작업이 얼마나 오래 걸릴지 모르기 때문에 이것은 정말 나쁜 생각입니다. 최소한 의 시간 동안 만 잠금을 유지해야합니다 . 스레드 가 메소드가 아닌 잠금을 소유하고 있기 때문에 잠재적으로 불가능 합니다. 작업 스케줄러에 따라 동일한 스레드에서 나머지 비동기 메소드를 실행할 수도 없습니다.

  • 대기에서 잠금을 해제하고 대기가 리턴되면 다시 확보하십시오.
    이것은 가장 놀라운 IMO의 원칙을 위반합니다. 여기서 비동기 메소드는 동등한 동기 코드와 최대한 비슷하게 동작해야합니다 Monitor.Wait. 잠금 블록에서 사용하지 않으면 블록이 지속되는 동안 잠금을 소유하십시오.

기본적으로 여기에는 두 가지 경쟁 요구 사항이 있습니다. 첫 번째 시도시도 두 번째 접근법을 원한다면 두 개의 분리 된 잠금 블록을 await 표현식으로 구분하여 코드를 훨씬 명확하게 만들 수 있습니다.

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

따라서 잠금 블록 자체를 기다리는 것을 금지함으로써 언어는 실제로 수행 하려는 작업 에 대해 생각 하고 작성하는 코드에서 선택을 명확하게 만듭니다.


답변

이것은 이 답변 의 확장 일뿐 입니다.

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


답변

이것은 http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , Windows 8 앱 스토어 및 .net 4.5를 나타냅니다.

여기에 내 각도가 있습니다.

비동기 / 기다리는 언어 기능은 많은 것들을 상당히 쉽게 만들어 주지만 비동기 호출을 사용하기가 쉽지 않은 재진입 시나리오를 소개합니다.

많은 이벤트의 경우 이벤트 핸들러에서 돌아온 후 발생하는 상황에 대한 단서가 없기 때문에 이는 이벤트 핸들러에 특히 해당됩니다. 실제로 발생할 수있는 한 가지는 첫 번째 이벤트 핸들러에서 대기중인 비동기 메소드가 여전히 동일한 스레드의 다른 이벤트 핸들러에서 호출된다는 것입니다.

다음은 Windows 8 App Store 앱에서 발생한 실제 시나리오입니다. 내 응용 프로그램에는 두 가지 프레임이 있습니다. 프레임으로 들어오고 나가는 프레임에서 일부 데이터를 파일 / 저장 장치에로드 / 보호하려고합니다. OnNavigatedTo / From 이벤트는 저장 및로드에 사용됩니다. 저장 및 로딩은 일부 비동기 유틸리티 기능 (예 : http://winrtstoragehelper.codeplex.com/)에 의해 수행됩니다 . )에 . 프레임 1에서 프레임 2로 또는 다른 방향으로 탐색 할 때 비동기로드 및 안전한 작업이 호출되어 대기합니다. 이벤트 핸들러는 비동기 = void를 반환하며 대기 할 수 없습니다.

그러나 유틸리티의 첫 번째 파일 열기 작업 (저장 : 내부 함수라고 함)도 비동기이므로 첫 번째 대기는 프레임 워크에 제어를 반환합니다.이 프레임은 나중에 두 번째 이벤트 핸들러를 통해 다른 유틸리티 (로드)를 호출합니다. 로드는 이제 동일한 파일을 열려고 시도하고 저장 조작을 위해 파일을 지금 여는 경우 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);
    }
}

그의 잠금은 기본적으로 하나의 잠금으로 유틸리티의 모든 파일 작업을 잠급니다. 이는 불필요하게 강력하지만 내 시나리오에는 잘 작동합니다.

여기 내 테스트 프로젝트가 있습니다 : 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


답변

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대신 사용하십시오. 리더 / 라이터 잠금은 얻지 못하지만 구현을 시도하고 테스트해야합니다.


답변

흠, 못생긴 것처럼 보이고 작동하는 것 같습니다.

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