C#におけるタイマースレッドの生存期間

C#ではスレッドやタスクを簡単に扱えるようになっています。
また、ガベージコレクタが走っており、不要なメモリ資源の回収も簡単に行えます。

しかし、これらの便利な機能があるが故に落とし穴も存在します。

例えば、以下のコードはメソッドの中でスレッドプールからタイマーを生成して実行する例です。

    class Program
    {
        static void Main(string[] args)
        {
            System.Console.WriteLine("RunTimer() started.");
            RunThread();
            System.Console.WriteLine("RunTimer() exited.");

            // キー入力待機
            Console.ReadLine();
        }

        private static int count = 0;

        public static void RunThread(){
            System.Threading.Timer timer = new System.Threading.Timer(TimerProc, null, 100, 100);

            // キー入力待機
            Console.ReadLine();
        }

        private static void TimerProc(object state)
        {
            System.Console.WriteLine("count = " + count++);
        }
    }

Windows7 64bit、Visual Studio 2013の環境では、アプリケーションが終了するまでカウント値が出力され続けます。
一方、以下のようにRunTimerメソッドの実行後に強制的にガベージコレクタを実行すると、カウント値の出力がとまります。

    class Program
    {
        static void Main(string[] args)
        {
            System.Console.WriteLine("RunTimer() started.");
            RunThread();
            System.Console.WriteLine("RunTimer() exited.");
            GC.Collect();

            // キー入力待機
            Console.ReadLine();
        }

        private static int count = 0;

        public static void RunThread(){
            System.Threading.Timer timer = new System.Threading.Timer(TimerProc, null, 100, 100);

            // キー入力待機
            Console.ReadLine();
        }

        private static void TimerProc(object state)
        {
            System.Console.WriteLine("count = " + count++);
        }
    }

これは何故でしょうか?

RunThreadメソッドを抜けるとtaskオブジェクトはどこからも参照されなくなります。
ここで、C#のガベージコレクタが走り、参照されなくなったtaskオブジェクトをゴミとみなして解放されるようになります。
しかし、解放されるタイミングは実行環境に依存します。

上記の例ではプログラムがとても短かったので、ガベージコレクタを強制的に実行しない限りタイマースレッドの資源回収が行われなかったためです。
したがって、このようなコードを書くと後々で難解なバグに悩まされることになります。

解決策は、生成したスレッドを必ず何らかの方法で参照させ続けておくことです。
例で示したコードのタイマースレッドは、フィールドに参照を持たせておくことでガベージコレクタの実行後もカウント値が出力され続けます。

    class Program
    {
        static void Main(string[] args)
        {
            System.Console.WriteLine("RunTimer() started.");
            RunThread();
            System.Console.WriteLine("RunTimer() exited.");
            GC.Collect();

            // キー入力待機
            Console.ReadLine();
        }

        private static int count = 0;
        private static System.Threading.Timer timer;

        public static void RunThread(){
            timer = new System.Threading.Timer(TimerProc, null, 100, 100);

            // キー入力待機
            Console.ReadLine();
        }

        private static void TimerProc(object state)
        {
            System.Console.WriteLine("count = " + count++);
        }
    }

スレッドを実行するときはきちんと参照を保持しておくこと。
そして、スレッドを止めるときは明示的に止めることを意識しておけば問題ないと思います。

■参考サイト
Timer クラス
c# ファイナライザーの実行タイミング