Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# - 상호 참조하는 경우의 정적 생성자 동작 방식

정적 생성자가 싱글톤(Singleton) 객체를 생성하는 데 가장 최적의 장소가 된 배경에는 CLR의 노력이 있습니다. 왜냐하면 정적 생성자에 대한 실행을 CLR 측에서 단 하나의 스레드만 접근하도록 보장해 주기 때문입니다.

이 때, 재미있는 현상이 하나 나오는데요. 정적 생성자가 서로 상호 참조했을 때 어떤 현상이 발생할까입니다. 일단, 예를 들기 위해 다음과 같은 코드를 만들어,

using System;

class Program
{
    static void Main(string[] args)
    {
        BClass.Initialize();
    }
}

class AClass
{
    public static int Flag = 5;
    static AClass()
    {
        Flag = 10;
    }

    internal static void Initialize() { }
}

class BClass
{
    static BClass()
    {
        AClass.Initialize();
        Console.WriteLine($"{nameof(AClass)}.Flag == " + AClass.Flag); // 출력 값: AClass.Flag == 10 
    }

    internal static void Initialize() { }
}

실행해 보면, AClass의 정적 생성자 실행으로 인해 화면에는 10의 값이 출력됩니다. 그런데, 이것이 언제나 그런 것은 아닙니다. 바로 정적 생성자에서 상호 참조되는 경우인데, 예제를 다음과 같이 바꿔보면 알 수 있습니다.

using System;

class Program
{
    static void Main(string[] args)
    {
        AClass.Initialize();
    }
}

class AClass
{
    public static int Flag = 5;
    static AClass()
    {
        Console.WriteLine($"{nameof(AClass)}.cctor - start");
        BClass.Initialize();
        Flag = 10;
        Console.WriteLine($"{nameof(AClass)}.cctor - end");
    }

    internal static void Initialize() { }
}

class BClass
{
    static BClass()
    {
        Console.WriteLine($"{nameof(BClass)}.cctor - start");
        AClass.Initialize();
        Console.WriteLine($"{nameof(BClass)}.cctor - end: {nameof(AClass)}.Flag == " + AClass.Flag);
    }

    internal static void Initialize() { }
}

보시는 바와 같이, BClass의 정적 생성자는 (로직상) 코드가 바뀌지 않았지만, 이 코드를 실행해 보면 다음과 같은 결과를 볼 수 있습니다.

AClass.cctor - start
BClass.cctor - start
BClass.cctor - end: AClass.Flag == 5
AClass.cctor - end

즉, CLR은 정적 생성자에서 상호 참조로 인한 cctor가 실행되는 경우 dead-lock 상황을 유발하지 않는 대신 중첩시켜 실행을 해버립니다. 물론, 그 부작용으로 개발자가 당연하다고 기대했던 AClass.Flag의 값이 10에서 5로 바뀌어 버렸습니다. 간단히 말해, 전에는 잘 동작했던 코드가 어느 순간 오동작을 하는 것처럼 바뀔 수 있다는 것입니다.

"제프리 리처의 CLR via C# - 4판"에 보면 바로 이와 같은 상황때문에 정적 생성자에서는 가능한 다른 정적 생성자를 호출하는 상황에 주의를 기울이라고 나옵니다.

역시... 제프리 리처의 책은 믿고 보게 됩니다. ^^

(첨부한 파일은 위의 예제 코드를 담고 있습니다.)




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







[최초 등록일: ]
[최종 수정일: 9/2/2015]

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

비밀번호

댓글 작성자
 



2015-09-03 01시45분
[이성환] 어떻게 보면 당연한 결과인 것 처럼 느끼지는데요.
아래 예제에서는 AClass의 정적 생성자 호출이 끝나기 전에 Console.WriteLine($"{nameof(BClass)}.cctor - end: {nameof(AClass)}.Flag == " + AClass.Flag); 를 호출했으니
- 아직 AClass.Flag = 10 구문 호출 전이기 때문에 - 당연히 AClass.Flag == 5 가 찍히는 것일테고
AClass의 정적 생성자 호출이 완료된 시점에서 다시 찍어보면 AClass.Flag == 10 으로 나옵니다.

글의 요지는 정적 생성자 호출이 완료되기 전에 다른 정적 생성자를 호출한 값을 할당하고 사용하는 것에 주의를 기울이라는 의미인가요?

"CLR은 정적 생성자에서 상호 참조로 인한 cctor가 실행되는 경우 dead-lock 상황을 유발하지 않는 대신 중첩시켜 실행을 해버립니다. " 라는 문장이 살짝 이해가 되지 않아 질문 남겨봅니다...
[guest]
2015-09-03 02시18분
가령 BClass를 B 개발자가 만들고 있다고 생각해 보겠습니다. BClass는 당연히 AClass의 정적 멤버를 호출하면서 정적 생성자의 실행이 완료되었다고 생각할 것이고, 당연히 AClass.Flag의 값은 10이어야 합니다.

그런데, 어느날 AClass를 만든 A 개발자가 필요에 의해서 BClass의 정적 멤버를 AClass의 정적 생성자에서 호출하는 코드를 넣어버린 것입니다. 그 순간 B 개발자도 모르게 BClass의 정적 생성자가 오동작을 하게 되는 문제가 발생하는 것입니다.

사실 명시적으로 호출되는 경우라면 이런 것이 문제가 되지 않을 수 있지만, 정적 생성자라는 것이 암시적으로 JIT 컴파일 시에 호출 코드가 삽입되기 때문에 개발자가 의도치 않게 문제 상황을 접할 수 있음을 설명한 것입니다.

만약 저런 부작용을 없애려면 대상 정적 생성자가 완전히 실행 완료될 때까지 대기해야 하는데, 그렇게 되면 AClass의 cctor에서는 BClass의 cctor가 실행완료될 때까지 기다려야 하고, BClass의 cctor 또한 AClass의 cctor가 완료될 때까지 대기하게 되면 dead-lock이 발생하게 됩니다.
정성태
2015-09-03 07시47분
[이성환] 네 설명해주신 부분 이해했습니다. "제프리 리처의 CLR via C# - 4판" 에서는 아주 짦막하게 언급하고 지나간 부분이었는데 이렇게 설명해주시니 더 이해가 잘 되네요. =ㅂ=b

그런데 궁금한 건 "제프리 리처의 CLR via C# - 4판" 에서는 정적 생성자가 완료될 때까지 lock을 획득하는 방식으로 처리된다고 되어 있는데

그럼 본문에서 설명하신 " dead-lock 상황을 유발하지 않는 대신 중첩시켜 실행을 해버린다"의 의미는 완료되지 않은 정적 생성자를 다시 수행하지 않고 곧바로 현재 정적 멤버에 접근하다는 얘기인가요?
(이 예제에서는 같은 스레드라 상관없어 보이긴 하지만서도...)

테스트를 돌려보면 그렇게 동작하는 거 같은데 정확한 의미인지 궁금해서 다시 질문드립니다.
[guest]
2015-09-03 07시53분
@이성환, 네 사실 ^^ 저도 제프리의 (lock을 획득한다는) 그 부분이 정말인지 확인하려고 위의 테스트를 했던 것입니다. (첨부 파일을 다운로드해보시면 다중 스레드에서도 테스트 한 예제가 있습니다.) 하지만 실제 테스트를 해보니, lock을 건다고 해서 dead-lock을 유발시키는 식으로 처리하진 않고 대신 '한번만 실행'시킨다는 목적에 충실하게 하는 듯합니다.
정성태

1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13382정성태6/25/20233482.NET Framework: 2130. C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)파일 다운로드1
13381정성태6/25/20233869오류 유형: 869. Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
13380정성태6/24/20233306스크립트: 52. 파이썬 3.x에서의 동적 함수 추가
13379정성태6/23/20233309스크립트: 51. 파이썬 2.x에서의 동적 함수 추가
13378정성태6/22/20233208오류 유형: 868. docker - build 시 "CANCELED ..." 뜨는 문제
13377정성태6/22/20237083오류 유형: 867. 파이썬 mysqlclient 2.2.x 설치 시 "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 오류
13376정성태6/21/20233429.NET Framework: 2129. C# - Polly를 이용한 클라이언트 측의 요청 재시도파일 다운로드1
13375정성태6/20/20233110스크립트: 50. Transformers (신경망 언어모델 라이브러리) 강좌 - 2장 코드 실행 결과
13374정성태6/20/20233216오류 유형: 866. 파이썬 - <class 'AttributeError'> module 'flask.json' has no attribute 'JSONEncoder'
13373정성태6/19/20234536오류 유형: 865. 파이썬 - pymssql 설치 관련 오류 정리
13372정성태6/15/20233214개발 환경 구성: 682. SQL Server TLS 통신을 위해 사용되는 키 길이 확인 방법
13371정성태6/15/20233245개발 환경 구성: 681. openssl - 인증서 버전(V1 / V3)
13370정성태6/14/20233417개발 환경 구성: 680. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 - TLS 1.2 지원
13369정성태6/13/20233239개발 환경 구성: 679. PyCharm(을 비롯해 JetBrains에 속한 여타) IDE에서 내부 Window들의 탭이 없어진 경우
13368정성태6/13/20233373개발 환경 구성: 678. openssl로 생성한 인증서를 SQL Server의 암호화 인증서로 설정하는 방법
13367정성태6/10/20233505오류 유형: 864. openssl로 만든 pfx 인증서를 Windows Server 2016 이하에서 등록 시 "The password you entered is incorrect" 오류 발생
13366정성태6/10/20233284.NET Framework: 2128. C# - 윈도우 시스템에서 지원하는 암호화 목록(Cipher Suites) 나열파일 다운로드1
13365정성태6/8/20233022오류 유형: 863. MODIFY FILE encountered operating system error 112(failed to retrieve text for this error. Reason: 15105)
13364정성태6/8/20233819.NET Framework: 2127. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 [1]
13363정성태6/7/20233378스크립트: 49. 파이썬 - "Transformers (신경망 언어모델 라이브러리) 강좌" - 1장 2절 코드 실행 결과
13362정성태6/1/20233322.NET Framework: 2126. C# - 서버 측의 요청 제어 (Microsoft.AspNetCore.RateLimiting)파일 다운로드1
13361정성태5/31/20233750오류 유형: 862. Facebook - ASP.NET/WebClient 사용 시 graph.facebook.com/me 호출에 대해 403 Forbidden 오류
13360정성태5/31/20233138오류 유형: 861. WSL/docker - failed to start shim: start failed: io.containerd.runc.v2: create new shim socket
13359정성태5/19/20233468오류 유형: 860. Docker Desktop - k8s 초기화 무한 반복한다면?
13358정성태5/17/20233798.NET Framework: 2125. C# - Semantic Kernel의 Semantic Memory 사용 예제 [1]파일 다운로드1
13357정성태5/16/20233576.NET Framework: 2124. C# - Semantic Kernel의 Planner 사용 예제파일 다운로드1
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...