Microsoft MVP성태의 닷넷 이야기
닷넷: 2285. C# - async 메서드에서의 System.Threading.Lock 잠금 처리 [링크 복사], [링크+제목 복사],
조회: 8253
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 7개 있습니다.)
.NET Framework: 2064. C# - Mutex와 Semaphore/SemaphoreSlim 차이점
; https://www.sysnet.pe.kr/2/0/13156

.NET Framework: 2065. C# - Mutex의 비동기 버전
; https://www.sysnet.pe.kr/2/0/13157

닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
; https://www.sysnet.pe.kr/2/0/13555

닷넷: 2217. C# - 최댓값이 1인 SemaphoreSlim 보다 Mutex 또는 lock(obj)를 선택하는 것이 나은 이유
; https://www.sysnet.pe.kr/2/0/13558

디버깅 기술: 195. windbg 분석 사례 - Semaphore 잠금으로 인한 Hang 현상 (닷넷)
; https://www.sysnet.pe.kr/2/0/13560

닷넷: 2284. C# - async 메서드에서의 lock/Monitor.Enter/Exit 잠금 처리
; https://www.sysnet.pe.kr/2/0/13697

닷넷: 2285. C# - async 메서드에서의 System.Threading.Lock 잠금 처리
; https://www.sysnet.pe.kr/2/0/13698




C# - async 메서드에서의 System.Threading.Lock 잠금 처리

.NET 9부터 새롭게 도입한 System.Threading.Lock 타입 역시 Monitor 및 Mutex와 그대로 치환이 됩니다. 즉, lock을 소유한 스레드를 알고 있으며 재진입에 따른 잠금 횟수를 관리합니다.

따라서, 다음의 코드는,

internal class Program
{
    static Lock _obj = new();

    static void Main(string[] args)
    {
        _obj.Enter();
        
        Thread t = new Thread(() =>
        {
            _obj.Exit(); // 실행 시: System.Threading.SynchronizationLockException: 'The calling thread does not hold the lock.'
        });

        t.Start();

        t.Join();
    }
}

실행 시점에 Exit를 호출하는 코드에서 예외가 발생합니다. 마찬가지로 다음의 코드도,

internal class Program
{
    static Lock _obj = new();
    static void Main(string[] args)
    {
        _obj.Enter();
        _obj.Enter(); // 2번 잠금

        Thread t = new Thread(() =>
        {
            _obj.Enter(); // 다른 스레드에서 lock을 얻으려고 시도
            Console.WriteLine("Thread 1");
            _obj.Exit();
        });

        t.Start();

        _obj.Exit(); // 한 번 잠금을 해제
        Thread.Sleep(5000);
        _obj.Exit(); // 두 번 잠금을 해제 - 이 시점에 "Thread 1"이 출력됨

        t.Join();
    }
}

Main 스레드에서 _obj.Exit를 2번 호출한 시점에야 lock이 풀려 Thread의 내부 코드가 실행됩니다.




비동기 문맥에서도 역시 Monitor와 동일한 동작을 보이는데, lock 예약어를 이용한 코드는 아예 컴파일 단계에서 오류를 발생시키고,

internal class Program
{
    static Lock _obj = new();

    static async Task Main(string[] args)
    {
        lock (_obj)
        {
            await Task.Delay(2000); // error CS4007: Instance of type 'System.Threading.Lock.Scope' cannot be preserved across 'await' or 'yield' boundary.
        }
    }
}

Enter/Exit로 풀어내면 컴파일 오류는 피할 수 있지만, Exit 시점에 여전히 예외는 발생합니다.

internal class Program
{
    static Lock _obj = new();

    static async Task Main(string[] args)
    {
        _obj.Enter();

        await Task.Delay(2000);

        // 컴파일은 되지만,
        _obj.Exit(); // 실행 시: System.Threading.SynchronizationLockException: 'The calling thread does not hold the lock.'
    }
}

SynchronizationContext 환경을 제공하는 Windows Forms/WPF까지 Enter/Exit로 풀어내 사용하는 것은 Monitor와 일치합니다.

Lock _obj = new();

private async void Form1_Load(object sender, EventArgs e)
{
    _obj.Enter();

    try
    {
        await Task.Delay(2000);
    }
    finally
    {
        _obj.Exit(); // SynchronizationContext에 따라 Enter를 호출한 스레드에서 Exit를 호출하므로 동기화 성공
    }
}

유의해야 할 점은, (lock 예약어가 내부적으로 사용하는) Scope를 사용한 경우에는 컴파일 오류가 발생한다는 점입니다.

var scope = _obj.EnterScope();

try
{
    await Task.Delay(2000);
}
finally
{
    scope.Dispose(); // 컴파일 오류: error CS4007: Instance of type 'System.Threading.Lock.Scope' cannot be preserved across 'await' or 'yield' boundary.
}

따라서 SynchronizationContext가 있음을 알고 의도적으로 System.Threading.Lock을 사용하는 경우라면 Enter/Exit를 사용해야 합니다. (참고로, scope.Dispose 코드를 주석 처리하면 컴파일은 되지만, 당연히 lock은 해제되지 않아 결국엔 문제가 발생합니다.)

정리하면, System.Threading.Lock 역시 (스레드가 바뀌는) 비동기 문맥에서는 사용할 수 없고, 그런 경우를 원한다면 Semaphore(Slim)을 사용해야 합니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 8/6/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12211정성태4/27/202019259개발 환경 구성: 486. WSL에서 Makefile로 공개된 리눅스 환경의 C/C++ 소스 코드 빌드
12210정성태4/20/202020691.NET Framework: 903. .NET Framework의 Strong-named 어셈블리 바인딩 (1) - app.config을 이용한 바인딩 리디렉션 [1]파일 다운로드1
12209정성태4/13/202017404오류 유형: 614. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우 (2)
12208정성태4/12/202015965Linux: 29. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우
12207정성태4/2/202015799스크립트: 19. Windows PowerShell의 NonInteractive 모드
12206정성태4/2/202018424오류 유형: 613. 파일 잠금이 바로 안 풀린다면? - The process cannot access the file '...' because it is being used by another process.
12205정성태4/2/202015093스크립트: 18. Powershell에서는 cmd.exe의 명령어를 지원하진 않습니다.
12204정성태4/1/202015089스크립트: 17. Powershell 명령어에 ';' (semi-colon) 문자가 포함된 경우
12203정성태3/18/202017936오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/202021196개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/202018434오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token". [1]
12200정성태3/18/202018523VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/202016167오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/202019528오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/202018811VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기 [1]
12196정성태3/17/202015942오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/16/202018262.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/202020992오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/202017903개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행 [1]
12192정성태3/14/202019924개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/202021040개발 환경 구성: 483. docker - OracleXE 컨테이너 실행 [1]
12190정성태3/14/202015622오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/202021205개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태3/13/202026020Windows: 169. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/202015576오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/202017398오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...