前期提要:去年分享過重啟網站才能解決的 TypeInitializationException 錯誤,依當時觀察與研究,靜態建構式或靜態欄位初始化只會執行一次,若發生錯誤 TypeInitializationException 會類似被 Cache 住,後續試圖存取該型別時.NET Runtime 會直接拋出前次產生的 TypeInitializationException。(此點可由錯誤訊息埋入的時間戳記證實)

實驗結果明確,足以證實 TypeInitializationException 會被 Cache 住,但我沒找到相關文件明確描述此一行為。曾試著用 Debugger 找出 Cache 運作過程,但它發生在 .NET Runtime 內部,按 F11 Step Into 也進不去,像是踩到 .NET CLR 的結界,凡人如我,無法再前進半步,只得抱憾而歸。

前幾天看到忠成老師在臉書專頁分享了 一篇文章,有這麼一段結論:

在C# 中,static constructor 即使被執行一次,如果裡面發生了例外﹑那麼 static constructor 依舊會遵循只執行一次的規則,後續對於此類別的建立或使用,就不會再執行 static constructor,而這個例外會被重複使用,簡略的說,例外被 re-use(cached)

這算是我第一次看到有人明確描述 TypeInitializationException Cache 現象。在貼文留言分享我曾追到卡住的經歷,忠成老師信手拈來,隨手貼了連結:

在我心中久懸的謎,就這麼解開了解開了解開了~~

我知道 .NET Runtime 是開源的,原以為會是一堆看不懂的低階 C 語言,所以沒認真想過可從原始碼下手,結果 Core CLR 是用 C# 寫的(但有不少 Unsafe 部分),處理靜態建構式的程式碼在 System.Runtime.CompilerServices.ClassConstructorRunner,所謂記下 TypeInitializationException 重複利用的程式碼在 public static unsafe void EnsureClassConstructorRun(StaticClassConstructionContext* pContext) 方法:(我在關鍵處加了中文註解)

public static unsafe void EnsureClassConstructorRun(StaticClassConstructionContext* pContext)
{
    IntPtr pfnCctor = pContext->cctorMethodAddress;
    NoisyLog("EnsureClassConstructorRun, cctor={0}, thread={1}", pfnCctor, CurrentManagedThreadId);

    // If we were called from MRT, this check is redundant but harmless. This is in case someone within classlib
    // (cough, Reflection) needs to call this explicitly.
    if (pContext->initialized == 1)
    {
        NoisyLog("Cctor already run, cctor={0}, thread={1}", pfnCctor, CurrentManagedThreadId);
        return;
    }
    // 物件可能有多個靜態建構式陣列(例如:泛型型別 SomeClass<string>、SomeClass<int> 各自有靜態建構式 Instance)
    CctorHandle cctor = Cctor.GetCctor(pContext);
    Cctor[] cctors = cctor.Array;
    int cctorIndex = cctor.Index;
    try
    {
        Lock cctorLock = cctors[cctorIndex].Lock;
        if (DeadlockAwareAcquire(cctor, pfnCctor))
        {
            int currentManagedThreadId = CurrentManagedThreadId;
            try
            {
                NoisyLog("Acquired cctor lock, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);

                cctors[cctorIndex].HoldingThread = currentManagedThreadId;
                // 若還沒有初始化
                if (pContext->initialized == 0)  // Check again in case some thread raced us while we were acquiring the lock.
                {
                    // 檢查目前使用的靜態建構式 Exception 屬性是否有之前執行過留下的 TypeInitializationException
                    TypeInitializationException priorException = cctors[cctorIndex].Exception;
                    // 若有就拋出上次的 TypeInitializationException
                    if (priorException != null)
                        throw priorException;
                    try
                    {
                        NoisyLog("Calling cctor, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);

                        ((delegate*<void>)pfnCctor)();

                        // Insert a memory barrier here to order any writes executed as part of static class
                        // construction above with respect to the initialized flag update we're about to make
                        // below. This is important since the fast path for checking the cctor uses a normal read
                        // and doesn't come here so without the barrier it could observe initialized == 1 but
                        // still see uninitialized static fields on the class.
                        Interlocked.MemoryBarrier();

                        NoisyLog("Set type inited, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);

                        pContext->initialized = 1;
                    }
                    catch (Exception e)
                    {
                        // 若執行靜態建構式出錯,將例外包成 TypeInitializationException
                        TypeInitializationException wrappedException = new TypeInitializationException(null, SR.TypeInitialization_Type_NoTypeAvailable, e);
                        // 將 TypeInitializationException 存入該建構式的 Exception 屬性
                        cctors[cctorIndex].Exception = wrappedException;
                        throw wrappedException;
                    }
                }
            }
            finally
            {
                cctors[cctorIndex].HoldingThread = ManagedThreadIdNone;
                NoisyLog("Releasing cctor lock, cctor={0}, thread={1}", pfnCctor, currentManagedThreadId);

                cctorLock.Release();
            }
        }
        else
        {
            // Cctor cycle resulted in a deadlock. We will break the guarantee and return without running the
            // .cctor.
        }
    }
    finally
    {
        Cctor.Release(cctor);
    }
    NoisyLog("EnsureClassConstructorRun complete, cctor={0}, thread={1}", pfnCctor, CurrentManagedThreadId);
}

這段程式把我好奇的 TypeInitializationException Cache 原理解釋得一清二楚,自此無憾。術業有專攻,你心中的好漢坡,在別人眼中根本暖身運動,哈!

題外話,會開始看 C# in Depth 其實就是研究靜態建構式卡關後下的決心,而讀了幾章學到的東西還真的在今天派上用場。一個型別會有多個靜態建構式 Instance 這點,要不是在 C# in Depth 讀過,我應該想不到泛型情境,不知得花多少時間爬文解惑。程式寫得出來就好,學習硬梆梆的底層知識有屁用?今天老天爺也順手做了示範。

We know the static constructors will run only once and cache TypeInitializationException for later calls, let's see how it works from the coreclr source code.


Comments

Be the first to post a comment

Post a comment