ThreadAbort と EscalationPolicy

壊れた世界の続き (id:NyaRuRu:20060531:p1).
つまるところ,CLR 2.0 での Thread.Abort() は特定条件下で blocking method として振る舞うようですね.

  1. In the case of the catch block, the call to the Abort method does NOT block and the thread WILL abort as soon as the catch block completes. This is true even if no further code executes on the thread after the catch block.
  2. In the case of the finally block and the static constructor, the call to the Abort method appears to block until the finally block or static constructor completes! This is totally unexpected. If the thread doesn't execute any more code after the finally block or the static constructor, then ThreadAbortException is never thrown; but if the thread goes on executing long enough it WILL abort. (How long is "long enough"? Well, let's just say it's a race condition.)

とはいえ,プラグイン方式で読み込んだ外部アセンブリを実行する場合など,たかが脇役にこんな方法で邪魔されても困るという場合もあるでしょう.
そこで,世界の仕組みを書き換えます.



以前ちらっと書きましたが (id:NyaRuRu:20050318:p1),スレッドの停止処理にタイムアウトを設定し,ある時間を経過したら終了処理を強制終了に昇格させるよう,CLR の設定を変更する方法があります.
具体的には CLR Hosting API を使用して,次のような初期化を行います.

#define WIN32_LEAN_AND_MEAN
#include <mscoree.h>
#include <comdef.h>

#pragma comment (lib, "mscoree.lib")

_COM_SMARTPTR_TYPEDEF(ICLRRuntimeHost, __uuidof(ICLRRuntimeHost));
_COM_SMARTPTR_TYPEDEF(ICLRControl, __uuidof(ICLRControl));
_COM_SMARTPTR_TYPEDEF(ICLRPolicyManager, __uuidof(ICLRPolicyManager));

int wmain()
{
    CoInitialize(NULL);
    HRESULT hr = S_OK;

    {
        ICLRRuntimeHostPtr clrRuntimeHost;
        hr = CorBindToRuntimeEx(
            L"v2.0.50727", 
            NULL,        
            NULL, 
            CLSID_CLRRuntimeHost, 
            IID_ICLRRuntimeHost, 
            (PVOID*) &clrRuntimeHost);

        ICLRControlPtr clrControl;
        hr = clrRuntimeHost->GetCLRControl( &clrControl );

        ICLRPolicyManagerPtr clrPolicyManager;
        hr = clrControl->GetCLRManager( __uuidof(ICLRPolicyManager), (PVOID*)&clrPolicyManager );

        // 10*1000 msec 待って (graceful) ThreadAbort を RudeAbortThread に昇格させる
        hr = clrPolicyManager->SetTimeoutAndAction( OPR_ThreadAbort, 10*1000, eRudeAbortThread );

        clrRuntimeHost->Start();

        // Test.Program.MyMain の実行
        // ExecuteInDefaultAppDomain は static int Function( string args ) というシグネチャを要求することに注意
        //   戻り値 void, 引数 string[] など微妙な違いで失敗する
        DWORD retVal = 0;
        hr = clrRuntimeHost->ExecuteInDefaultAppDomain( L"Test.dll", L"Test.Program", L"MyMain", L"Hello from host!", &retVal );
    }

    CoUninitialize();

    return 0;
}

C# 側のコードは,前回とほとんど同じです.(若干デバッグメッセージを追加)

using System;
using System.Threading;

namespace Test
{
    public class Program
    {
        static void Proc()
        {
            try
            {
                try
                {
                }
                finally
                {
                    Console.WriteLine("Enter Sleep");
                    Thread.Sleep(Timeout.Infinite);
                    Console.WriteLine("Exit Sleep");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception " + e.ToString());
                throw;
            }
        }
        static void Test()
        {
            Thread worker = new Thread(new ThreadStart(Proc));
            worker.Name = "Worker Thread";
            worker.Start();
            Thread.Sleep(1000);
            worker.Abort();
            Console.WriteLine("Aborted");
            worker.Join();
            Console.WriteLine("Joined");
        }
        public static int MyMain(string args)
        {
            Test();
            return 1;
        }
    }
}

ソースコード全体は以下にあります.
http://www.dwahan.net/nyaruru/hatena/EscalationPolicyTest.zip
このコードを実行すると,"Enter Sleep"と表示され,さらに約 10 秒が経過すると,以下のように終了します.

Enter Sleep
Aborted
Joined

"Exit Sleep"が表示されない,つまり finally 句が途中で中断していることに注意してください.finally 句は必ず実行されるという「常識」は,この段階ではもはや成り立ちません.どうしても終了処理を実行する必要がある場合は,CriticalFinalizerObject や Constrained Execution Regions を使用します.これらの処理すらも失敗する場合については被害度が自己申告されていて,「スレッドが未定義な状態になる」→「スレッドごと終了」,「AppDomainごと未定義な状態になる」→「AppDomainごと終了」,「プロセスごと未定義な状態になる」→「プロセス終了」,といった具合に色々と想定ユースケースが存在します.まあ詳しくは日を改めて.



このあたりの仕組みについて体系立てて書かれた本は『Customizing the Microsoft.NET Framework Common Language Runtime』しか知らないので,しゃれでなく本当にこんな暗黒面の知識が必要になってしまった方は一読されることをおすすめします.内容は .NET 2.0 beta 時のものですが,基本的な考え方の部分では十分参考になるでしょう.
Customizing the Microsoft® .NET Framework Common Language Runtime (Pro Developer)