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

... 91  92  93  94  [95]  96  97  98  99  100  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11559정성태6/25/201828650개발 환경 구성: 384. 영문 설정의 Windows 10 명령행 창(cmd.exe)의 한글 지원 [6]
11558정성태6/24/201822286.NET Framework: 775. C# 7.3 - unmanaged(blittable) 제네릭 제약파일 다운로드1
11557정성태6/22/201821751.NET Framework: 774. C# - blittable 타입이란?파일 다운로드1
11556정성태6/19/201828615.NET Framework: 773. C# 7.3 - 구조체의 고정 크기를 갖는 fixed 배열 필드에 대한 직접 접근 가능 [1]파일 다운로드1
11555정성태6/18/201820293.NET Framework: 772. C# 7.3 - 사용자 정의 타입에 fixed 적용 가능(Custom fixed)파일 다운로드1
11554정성태6/17/201822341.NET Framework: 771. C# 7.3 - 자동 구현 속성에 특성 적용 가능(Attribute on backing field)
11553정성태6/15/201821901.NET Framework: 770. C# 7.3 - 개선된 메서드 선택 규칙 3가지(Improved overload candidates)파일 다운로드1
11552정성태6/15/201823898.NET Framework: 769. C# 7.3에서 개선된 문법 4개(Support == and != for tuples, Ref Reassignment, Constraints, Stackalloc initializers)파일 다운로드1
11551정성태6/14/201820500개발 환경 구성: 383. BenchmarkDotNet 사용 시 주의 사항
11550정성태6/13/201820392.NET Framework: 768. BenchmarkDotNet으로 Span<T> 성능 측정 [2]
11549정성태6/13/201822072개발 환경 구성: 382. BenchmarkDotNet에서 생성한 BuildPlots.R 파일을 실행하는 방법
11548정성태6/13/201819272오류 유형: 470. .NET Core + BenchmarkDotNet 실행 시 프레임워크를 찾지 못하는 문제
11547정성태6/13/201824387.NET Framework: 767. BenchmarkDotNet 라이브러리 소개파일 다운로드1
11546정성태6/12/201824547.NET Framework: 766. C# 7.2의 특징 - GC 및 메모리 복사 방지를 위한 struct 타입 개선 [9]파일 다운로드1
11545정성태6/11/201822794오류 유형: 469. .NET Core 프로젝트를 Visual Studio에서 실행 시 System.BadImageFormatException 발생하는 경우 [1]
11544정성태6/10/201822383.NET Framework: 765. C# 7.2 - 숫자 리터럴의 선행 밑줄과 뒤에 오지 않는 명명된 인수
11543정성태6/9/201821961.NET Framework: 764. C# 7.2 - private protected 접근자 추가파일 다운로드1
11542정성태6/9/201860373개발 환경 구성: 381. Azure Web App 확장 예제 - Remove Custom Headers
11541정성태6/9/201819522개발 환경 구성: 380. Azure Web App 확장 배포 방법 [1]
11540정성태6/9/201820294개발 환경 구성: 379. Azure Web App 확장 예제 제작 [2]
11539정성태6/8/201820120.NET Framework: 763. .NET Core 2.1 - Tiered Compilation 도입파일 다운로드1
11538정성태6/8/201819408.NET Framework: 762. .NET Core 2.1 - 확장 도구(Tools) 관리 [1]
11537정성태6/8/201823914.NET Framework: 761. C# - SmtpClient로 SMTP + SSL/TLS 서버를 이용하는 방법 [5]
11536정성태6/7/201821564.NET Framework: 760. Microsoft Build 2018 - The future of C# 동영상 내용 정리 [1]파일 다운로드1
11535정성태6/7/201823369.NET Framework: 759. C# - System.Span<T> 성능 [1]
11534정성태6/6/201829109.NET Framework: 758. C# 7.2 - Span<T> [6]
... 91  92  93  94  [95]  96  97  98  99  100  101  102  103  104  105  ...