アンセーフなメソッドを使用してファイルなどリソースに対して排他制御を行わずにマルチスレッド書き込みを行うとファイルの内容がおかしくなったり問題が発生することは常識ですよね。以前から、実際どうなるのかを確認してみたかったので今回再現可能なマルチスレッド書き込みエラーを発生させるプログラムを作成しました。

動作確認環境

  • .NET 3.5, Windows XP 上で実施

今回作成したソリューションはこちらにアップしています。実行すればすぐにエラーを再現できます。

1. サンプルプログラムでエラーの再現

コンソールアプリケーションソリューションを作成し、Program.cs に次のソースのように編集します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;

namespace MultiThreadWrite
{
    class Program
    {
        static void Main(string[] args)
        {
            using (StreamWriter stream = new StreamWriter("test.txt", false, Encoding.GetEncoding("Shift_JIS"), 1024))
            //using (StreamWriter stream1 = new StreamWriter("test.txt", false, Encoding.GetEncoding("Shift_JIS"), 1024))
            //using (TextWriter stream = TextWriter.Synchronized(stream1))
            {

                List<Thread> threads = new List<Thread>();
                for (int i = 0; i < 2; ++i)
                {
                    ThreadUnsafeWriter w = new ThreadUnsafeWriter(stream, i.ToString().PadLeft(99));
                    Thread t = new Thread(new ThreadStart(w.DoWrite));
                    threads.Add(t);
                }
                threads.ForEach(delegate(Thread a) { a.Start(); });

                threads.ForEach(delegate(Thread a) { a.Join(); });
            }
            Console.WriteLine("PressEnter:");
            Console.ReadLine();
        }
    }

    public class ThreadUnsafeWriter
    {
        /// <summary>
        /// 
        /// </summary>
        public static TextWriter _writer;
        /// <summary>
        /// 
        /// </summary>
        private string _text = string.Empty;

        public ThreadUnsafeWriter(TextWriter writer, string text)
        {
            _writer = writer;
            _text = text;
        }

        public void DoWrite()
        {
            for (int i = 0; i < 10000; ++i)
            {
                _writer.WriteLine(_text);
            }
        }
    }
}

Visual Studio 上でデバッグ実行(F5)をすると、以下のIndexOutOfRangeException が発生しました。ファイルの中身を確認すると、同一行に複数のスレッドからの1行分の書き込みが混ざっていることが確認できます。

マルチスレッドによる書き込み時にTextWriter型(or StreamWriterなどの派生型) インスタンスをスレッドセーフにするには、自前で排他制御を行うのも手ですが、TextWriter.Synchronizedメソッドを使用することで、スレッドセーフなライタのラッパーを取得する方法が簡単で安全です。

サンプルプログラムでもMainメソッド直下のusing 行をコメントにし、コメント化されている2行のusing 部分のコメントを解除することで、プログラムを実行してもエラーが発生しなくなります。

2. まとめ

今回の説明は以上です。排他制御を行うのは当たり前のことなので、排他制御を行わなかったらどうなるかをテストしてみたくてサンプルを掲載しました。