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을 유발시키는 식으로 처리하진 않고 대신 '한번만 실행'시킨다는 목적에 충실하게 하는 듯합니다.
정성태

... 46  47  48  49  50  51  [52]  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12329정성태9/16/202012340오류 유형: 650. ASUS 메인보드 관련 소프트웨어 설치 후 ArmouryCrate.UserSessionHelper.exe 프로세스 무한 종료 현상
12328정성태9/16/202012524VS.NET IDE: 150. TFS의 이력에서 "Get This Version"과 같은 기능을 Git으로 처리한다면?
12327정성태9/12/202010139.NET Framework: 938. C# - ICS(Internet Connection Sharing) 제어파일 다운로드1
12326정성태9/12/20209642개발 환경 구성: 516. Azure VM의 Network Adapter를 실수로 비활성화한 경우
12325정성태9/12/20209215개발 환경 구성: 515. OpenVPN - 재부팅 후 ICS(Internet Connection Sharing) 기능이 동작 안하는 문제
12324정성태9/11/202010453개발 환경 구성: 514. smigdeploy.exe를 이용한 Windows Server 2016에서 2019로 마이그레이션 방법
12323정성태9/11/20209384오류 유형: 649. Copy Database Wizard - The job failed. Check the event log on the destination server for details.
12322정성태9/11/202010313개발 환경 구성: 513. Azure VM의 RDP 접속 위치 제한 [1]
12321정성태9/11/20208705오류 유형: 648. netsh http add urlacl - Error: 183 Cannot create a file when that file already exists.
12320정성태9/11/20209893개발 환경 구성: 512. RDP(원격 데스크톱) 접속 시 비밀 번호를 한 번 더 입력해야 하는 경우
12319정성태9/10/20209651오류 유형: 647. smigdeploy.exe를 Windows Server 2016에서 실행할 때 .NET Framework 미설치 오류 발생
12318정성태9/9/20209142오류 유형: 646. OpenVPN - "TAP-Windows Adapter V9" 어댑터의 "Network cable unplugged" 현상
12317정성태9/9/202011443개발 환경 구성: 511. Beats용 Kibana 기본 대시 보드 구성 방법
12316정성태9/8/20209877디버깅 기술: 170. WinDbg Preview 버전부터 닷넷 코어 3.0 이후의 메모리 덤프에 대해 sos.dll 자동 로드
12315정성태9/7/202012165개발 환경 구성: 510. Logstash - FileBeat을 이용한 IIS 로그 처리 [2]
12314정성태9/7/202010562오류 유형: 645. IIS HTTPERR - Timer_MinBytesPerSecond, Timer_ConnectionIdle 로그
12313정성태9/6/202011884개발 환경 구성: 509. Logstash - 사용자 정의 grok 패턴 추가를 이용한 IIS 로그 처리
12312정성태9/5/202015829개발 환경 구성: 508. Logstash 기본 사용법 [2]
12311정성태9/4/202011011.NET Framework: 937. C# - 간단하게 만들어 보는 리눅스의 nc(netcat), json_pp 프로그램 [1]
12310정성태9/3/202010266오류 유형: 644. Windows could not start the Elasticsearch 7.9.0 (elasticsearch-service-x64) service on Local Computer.
12309정성태9/3/202010014개발 환경 구성: 507. Elasticsearch 6.6부터 기본 추가된 한글 형태소 분석기 노리(nori) 사용법
12308정성태9/2/202011257개발 환경 구성: 506. Windows - 단일 머신에서 단일 바이너리로 여러 개의 ElasticSearch 노드를 실행하는 방법
12307정성태9/2/202012035오류 유형: 643. curl - json_parse_exception / Invalid UTF-8 start byte
12306정성태9/1/202010188오류 유형: 642. SQL Server 시작 오류 - error code 10013
12305정성태9/1/202011097Windows: 172. "Administered port exclusions"이 아닌 포트 범위 항목을 삭제하는 방법
12304정성태8/31/202010047개발 환경 구성: 505. 윈도우 - (네트워크 어댑터의 우선순위로 인한) 열거되는 IP 주소 순서를 조정하는 방법
... 46  47  48  49  50  51  [52]  53  54  55  56  57  58  59  60  ...