lock 문의 본문 내에서 'await'연산자를 사용할 수없는 이유는 무엇입니까?


348

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

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


27
허용되지 않는 이유를 찾은 것 같습니다.
asawyer

3
이 링크를 제안해도됩니다 : hanselman.com/blog/… 그리고 이것 하나 : blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx
hans

비동기 프로그래밍에 대해 조금 더 배우기 시작했습니다. 내 wpf 응용 프로그램에서 수많은 교착 상태가 발생한 후이 기사가 비동기 프로그래밍 실습에서 훌륭한 보호 수단이라는 것을 알았습니다. msdn.microsoft.com/ko-kr/magazine/…
C. Tewalt

잠금은 비동기 액세스가 코드를 깨뜨릴 때 비동기 액세스를 방지하도록 설계되었습니다. 잠금 내부에서 비동기를 사용하는 경우 잠금이 무효화되었습니다. 잠금 내부에서 무언가를 기다려야하는 경우 잠금을 올바르게 사용하지 않는 것
MikeT

답변:


366

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

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

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

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

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

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

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


190
캐시 항목을 반환해야하는 시나리오를 어떻게 처리하고 항목이 존재하지 않는 경우 내용을 비동기 적으로 계산 한 다음 항목을 추가 / 반환해야합니다.
Softlion

9
나는 여기 파티에 늦었다는 것을 알고 있지만 이것이 교착 상태를 나쁜 생각의 주요 원인으로 생각한 것에 놀랐습니다. 잠금 / 모니터의 재진입 특성이 문제의 더 큰 부분이 될 것이라는 내 생각에 결론을 내 렸습니다. 즉, 동기 세계에서는 별도의 스레드에서 실행되는 lock () 스레드 스레드에 두 개의 태스크를 큐에 넣습니다. 그러나 이제 await (허용 된 경우)를 사용하면 스레드가 재사용되었으므로 잠금 블록 내에서 두 가지 작업을 실행할 수 있습니다. 성대가 계속됩니다. 아니면 내가 잘못 이해 했습니까?
Gareth Wilson

4
@GarethWilson : 교착 상태 에 대한 질문이 있기 때문에 교착 상태에 대해 이야기했습니다 . 기괴한 재진입 문제가 가능하고 가능한 것 같습니다.
Eric Lippert

11
@ 에릭 리퍼 트. SemaphoreSlim.WaitAsync이 답변을 게시 한 후에 클래스가 .NET 프레임 워크에 추가 되었다는 것을 감안할 때 현재 가능하다고 가정 할 수 있다고 생각합니다. 그럼에도 불구하고, 그러한 구조를 구현하기가 어렵다는 당신의 의견은 여전히 ​​유효합니다.
Contango

7
"임의의 코드는 대기자가 호출자에게 제어권을 리턴 한 후 메소드가 재개되는 시간 사이에 실행됩니다."-다중 스레드 컨텍스트에서 비동기 / 대기가없는 경우에도 모든 코드에 해당됩니다. 다른 스레드는 임의의 코드를 "잠금 순서 역전을 일으키는 잠금을 수행 할 수 있으므로 교착 상태가 발생할 수 있습니다." 그렇다면 왜 이것이 async / await에서 특별한 의미가 있습니까? 비동기 / 대기에 특히 중요한 "코드가 다른 스레드에서 재개 될 수 있음"에 대한 두 번째 요점을 이해합니다.
bacar

291

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

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

10
이 방법이 최근에 .NET 프레임 워크에 도입되었으므로 비동기 / 대기 환경에서 잠금 개념이 잘 입증되었다고 가정 할 수 있습니다.
콘 탄고

5
자세한 내용은이 기사에서 "SemaphoreSlim"텍스트를 검색하십시오. Async / Await-비동기 프로그래밍의 우수 사례
Await- BobbyA

1
@JamesKo 모든 작업이 결과를 기다리고 있다면 그 Stuff주변에 어떤 방법도 보이지 않습니다 ...
Ohad Schneider

7
mySemaphoreSlim = new SemaphoreSlim(1, 1)처럼 작동하기 위해 초기화되어서 는 안 lock(...)됩니까?
Sergey

3
이 답변의 확장 버전을 추가했습니다 : stackoverflow.com/a/50139704/1844247
Sergey

67

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

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

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

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

기본적으로 여기에는 두 가지 경쟁 요구 사항이 있습니다. 첫 번째 시도시도 두 번째 접근법을 원한다면 두 개의 분리 된 잠금 블록을 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

7
@Contango : 음 아니다 같은 일. 특히, 세마포어는 특정 스레드에 묶여 있지 않습니다. 잠금 목표는 비슷하지만 상당한 차이가 있습니다.
Jon Skeet

@ JonSkeet 나는 이것이 매우 오래된 오래된 스레드라는 것을 알고 있지만 두 번째 방법으로 잠금을 사용하여 something () 호출이 어떻게 보호되는지 잘 모르겠습니다. 쓰레드가 무언가를 실행하고있을 때 다른 쓰레드도 참여할 수 있습니다! 여기에 뭔가 빠졌습니까?

@Joseph :이 시점에서 보호되지 않습니다. 두 번째 접근 방식으로, 다른 스레드에서 획득 / 릴리스 한 다음 다시 획득 / 릴리스하고 있음을 분명히 알 수 있습니다. Eric의 답변에 따르면 첫 번째 방법은 나쁜 생각이기 때문입니다.
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예외가 사이에 발생하는 경우 - 차단 WaitAsynctry세마포어 (교착 상태)를 출시하지 않을 것입니다. 다른 한편으로, WaitAsync호출을 try블록 으로 이동 시키면 잠금을 획득하지 않고 세마포어가 해제 될 수있는 또 다른 문제가 발생할 수있다. 이 문제가 설명 된 관련 스레드를 참조하십시오. stackoverflow.com/a/61806749/7889645
AndreyCh

16

이것은 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


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


이 코드를 사용하는 데주의해야 할 점이 있는지 궁금합니다. 누구나이 코드와 관련된 문제를 보여줄 수 있다면 알고 싶습니다. 그러나 async / await 잠금의 개념은 SemaphoreSlim.WaitAsync.NET 프레임 워크에서 와 같이 확실히 입증되었습니다 . 이 코드는 리더 / 라이터 잠금 개념을 추가하는 것입니다.
Contango

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.