マルチスレッドで1つのテキストファイルへ書き込みする - C#

マルチスレッドで1つのテキストファイルへ書き込みするコードを紹介します。

概要

テキストファイルに書き込むコードをこちらの記事で紹介しました。 シングルスレッドで書き込む場合は、紹介したコードで問題ありませんが、 マルチスレッドで複数のスレッドからファイルに書き込みをする場合は、タイミングによっては同時にファイルに書き込んでしまい、 ファイルに不整合が発生する可能性があります。
この記事では、複数のスレッドからファイルに書き込みをする際に競合が起きないようにするコードを紹介します。

マルチスレッドでの対応方法

テキストファイルに書き込みをするStreamWriterクラスはスレッドセーフではありません。 複数のスレッドから利用する場合は、TextWriter.Synchronized を利用してスレッドセーフのラッパーを取得する必要があります。
StreamReaderオブジェクトを作成し、TextWriter.Synchronized メソッドの引数に作成したStreamReaderオブジェクトを与え、スレッドセーフ化された、 TextWriter オブジェクトを取得してスレッドから利用します。

書式 (TextWriter.Synchronized メソッド)

TextWriter (TextWriterクラス) = TextWriter.Synchronized(StreamWriterオブジェクト);

プログラム

UI

下図のUIを作成します。ButtonとTextBoxを配置します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MultiThreadOperation
{
  public partial class FormTextWrite : Form
  {
    private TextWriter tw;
 
    public FormTextWrite()
    {
      InitializeComponent();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
      FileStream fs = new FileStream("data.txt", FileMode.OpenOrCreate, FileAccess.Write);
      StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);
      tw = TextWriter.Synchronized(sw);
 
      Task<bool> t1 = Task<bool>.Factory.StartNew(proc1);
      Task<bool> t2 = Task<bool>.Factory.StartNew(proc2);
      Task<bool> t3 = Task<bool>.Factory.StartNew(proc3);
      Task<bool> t4 = Task<bool>.Factory.StartNew(proc4);

      await Task<bool>.WhenAll(new Task<bool>[] { t1, t2, t3, t4 });

      tw.Close();
      sw.Close();
      fs.Close();

      textBox1.Text = "処理が完了しました。";
    }

    private bool proc1()
    {
      for (int i = 0; i < 5; i++) {
        tw.WriteLine("ぺんぎん");
        Thread.Sleep(250);
      }
      return true;
    }

    private bool proc2()
    {
      for (int i = 0; i < 5; i++) {
        tw.WriteLine("あひる");
        Thread.Sleep(250);
      }
      return true;
    }

    private bool proc3()
    {
      for (int i = 0; i < 5; i++) {
        tw.WriteLine("しろくま");
        Thread.Sleep(250);
      }
      return true;
    }


    private bool proc4()
    {
      for (int i = 0; i < 5; i++) {
        tw.WriteLine("にわとり");
        Thread.Sleep(250);
      }
      return true;
    }

  }
}

解説

FileStreamクラスを利用して、テキストファイルを書き込みモードで開きます。
開いたFileSteremオブジェクトから、テキスト書き込み用のStreamWriterオブジェクトを作成します。
  FileStream fs = new FileStream("data.txt", FileMode.OpenOrCreate, FileAccess.Write);
  StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);

StreamWriterオブジェクトはスレッドセーフではないため、スレッドセーフなStreamWriterオブジェクトのラッパーを作成します。TextWriter.Synchronizedメソッドを 呼び出し、第一引数にStreamWriterオブジェクトを与えます。Synchronizedメソッドの戻値に、スレッドセーフなTextWriterオブジェクトが返ります。
  tw = TextWriter.Synchronized(sw);

Taskオブジェクトを4つ作成し、同時に実行開始します。
  Task<bool> t1 = Task<bool>.Factory.StartNew(proc1);
  Task<bool> t2 = Task<bool>.Factory.StartNew(proc2);
  Task<bool> t3 = Task<bool>.Factory.StartNew(proc3);
  Task<bool> t4 = Task<bool>.Factory.StartNew(proc4);

最後にファイルを閉じる必要があるため、すべてのTaskオブジェクトの終了を待ちます。Taskの終了待機についてはこちらの記事も参照して下さい。
   await Task<bool>.WhenAll(new Task<bool>[] { t1, t2, t3, t4 });

すべてのTaskが終了した後、FileStreamを閉じます。
  tw.Close();
  sw.Close();
  fs.Close();

最後にテキストボックスにメッセージを表示します。
  textBox1.Text = "処理が完了しました。";

Taskの並列処理部分では、ファイルに文字列を5回書き込みます。1回書き込むごとに 250ミリ秒待機します。
  private bool proc1()
  {
    for (int i = 0; i < 5; i++) {
      tw.WriteLine("ぺんぎん");
      Thread.Sleep(250);
    }
    return true;
  }

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


[button1]をクリックします。処理が実行され、完了するとテキストボックスにメッセージが表示されます。

処理が完了すると、実行ファイルと同じディレクトリに "data.txt"ファイルが作成されます。


data.txtファイルの内容を確認します。20行あり、「ぺんぎん」「しろくま」「にわとり」「あひる」がそれぞれ5個ずつ書き込まれています。

参考: ノンスレッドセーフで書き込むとどうなるか

TextWriter.Synchronizedを利用せずに、StreamWriterをそのまま利用して複数スレッドから同時に書き込んだ場合の結果を確認します。

コード

以下のコードを準備します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MultiThreadOperation
{
  public partial class FormTextWrite : Form
  {
    private StreamWriter sw_ng;

    private async void button2_Click(object sender, EventArgs e)
    {
      FileStream fs = new FileStream("data.txt", FileMode.OpenOrCreate, FileAccess.Write);
      sw_ng = new StreamWriter(fs, Encoding.UTF8);

      Task<bool> t1 = Task<bool>.Factory.StartNew(proc1_ng);
      Task<bool> t2 = Task<bool>.Factory.StartNew(proc2_ng);
      Task<bool> t3 = Task<bool>.Factory.StartNew(proc3_ng);
      Task<bool> t4 = Task<bool>.Factory.StartNew(proc4_ng);

      await Task<bool>.WhenAll(new Task<bool>[] { t1, t2, t3, t4 });

      sw_ng.Close();
      fs.Close();

      textBox1.Text = "処理が完了しました。";
    }

    private bool proc1_ng()
    {
      for (int i = 0; i < 5; i++) {
        sw_ng.WriteLine("ぺんぎん");
        Thread.Sleep(250);
      }
      return true;
    }

    private bool proc2_ng()
    {
      for (int i = 0; i < 5; i++) {
        sw_ng.WriteLine("あひる");
        Thread.Sleep(250);
      }
      return true;
    }

    private bool proc3_ng()
    {
      for (int i = 0; i < 5; i++) {
        sw_ng.WriteLine("しろくま");
        Thread.Sleep(250);
      }
      return true;
    }


    private bool proc4_ng()
    {
      for (int i = 0; i < 5; i++) {
        sw_ng.WriteLine("にわとり");
        Thread.Sleep(250);
      }
      return true;
    }

  }
}

解説

先のコードから、TextWriter.Synchronizedの処理を外し、各スレッドでStreamWriter オブジェクトを参照してテキストファイルに書き込みをします。

実行結果

プロジェクトを実行し、書き込み処理を実行します。作成された "data.txt"ファイルを開いてファイル内容を確認します。
ほとんどの行で衝突が起きていないため、一見正常に動作していますが、9行目が「にわろりくま」になっており、その次の行は空行になっています。同時に書き込みが実行され、 衝突した結果と考えられます。


衝突は実行状況によって変わるため、毎回違った結果になります。

著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2020-05-27
iPentec all rights reserverd.