web-dev-qa-db-ja.com

スレッドセーフな方法でファイルに書き込む

Stringbuilderをファイルに非同期的に書き込みます。このコードはファイルを制御し、ストリームを書き込み、リリースします。非同期操作からのリクエストを処理します。これはいつでも受信できます。

FilePathはクラスインスタンスごとに設定されます(したがって、ロックObjectはインスタンスごとに設定されます)が、これらのクラスはFilePathsを共有する可能性があるため、競合の可能性があります。この種の競合は、クラスインスタンスの外部からの他のすべてのタイプと同様に、再試行に対処されます。

このコードはその目的に適していますか?これを処理するより良い方法はありますか?これは、キャッチアンドリトライメカニックへの依存が少ない(またはない)ことを意味しますか?

また、他の理由で発生した例外をキャッチしないようにするにはどうすればよいですか。

public string Filepath { get; set; }
private Object locker = new Object();

public async Task WriteToFile(StringBuilder text)
    {
        int timeOut = 100;
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        while (true)
        {
            try
            {
                //Wait for resource to be free
                lock (locker)
                {
                    using (FileStream file = new FileStream(Filepath, FileMode.Append, FileAccess.Write, FileShare.Read))
                    using (StreamWriter writer = new StreamWriter(file, Encoding.Unicode))
                    {
                        writer.Write(text.ToString());
                    }
                }
                break;
            }
            catch
            {
                //File not available, conflict with other class instances or application
            }
            if (stopwatch.ElapsedMilliseconds > timeOut)
            {
                //Give up.
                break;
            }
            //Wait and Retry
            await Task.Delay(5);
        }
        stopwatch.Stop();
    }
26
Nathan Cooper

これにどのようにアプローチするかは、あなたが書いている頻度に大きく依存します。比較的少量のテキストをかなり頻繁に記述しない場合は、静的ロックを使用して完了です。ディスクドライブは一度に1つの要求しか満たすことができないため、どのような場合でもそれが最善の策かもしれません。すべての出力ファイルが同じドライブにあると仮定すると(おそらく公平な仮定ではありませんが、私に耐えてください)、アプリケーションレベルでのロックとOSレベルで行われるロックに大きな違いはありません。

lockerを次のように宣言すると:

static object locker = new object();

プログラム内の他のスレッドと競合しないことが保証されます。

このことを防弾(または少なくとも合理的に)にしたい場合は、例外をキャッチすることから逃れることはできません。悪いことが起こる可能性があります。あなたはmust何らかの方法で例外を処理する必要があります。エラーが発生した場合に行うことは、まったく別のことです。ファイルがロックされている場合は、おそらく数回再試行する必要があります。不正なパスまたはファイル名のエラー、ディスクの空き容量、またはその他のエラーが発生した場合は、おそらくプログラムを強制終了することをお勧めします。繰り返しますが、それはあなた次第です。ただし、エラーでプログラムがクラッシュすることに問題がない限り、例外処理を避けることはできません。

ところで、このコードはすべて置き換えることができます。

                using (FileStream file = new FileStream(Filepath, FileMode.Append, FileAccess.Write, FileShare.Read))
                using (StreamWriter writer = new StreamWriter(file, Encoding.Unicode))
                {
                    writer.Write(text.ToString());
                }

単一の呼び出しで:

File.AppendAllText(Filepath, text.ToString());

.NET 4.0以降を使用していると仮定します。 File.AppendAllText を参照してください。

これを処理できるもう1つの方法は、スレッドにメッセージをキューに書き込み、そのキューを処理する専用のスレッドを用意することです。 BlockingCollectionのメッセージと関連するファイルパスがあります。例えば:

class LogMessage
{
    public string Filepath { get; set; }
    public string Text { get; set; }
}

BlockingCollection<LogMessage> _logMessages = new BlockingCollection<LogMessage>();

スレッドはそのキューにデータを書き込みます。

_logMessages.Add(new LogMessage("foo.log", "this is a test"));

そのキューを処理するだけの長時間実行バックグラウンドタスクを開始します。

foreach (var msg in _logMessages.GetConsumingEnumerable())
{
    // of course you'll want your exception handling in here
    File.AppendAllText(msg.Filepath, msg.Text);
}

ここでの潜在的なリスクは、スレッドが速すぎるメッセージを作成し、消費者が追いつかないためにキューが無限に大きくなることです。それがアプリケーションの本当のリスクであるかどうかは、あなただけが言えることです。リスクがあると思われる場合は、キューに最大サイズ(エントリ数)を設定して、キューサイズがその値を超えた場合、プロデューサーがキューに空きができるまで待ってから追加できるようにします。

39
Jim Mischel

ReaderWriterLock を使用することもできます。これは、読み取り書き込み操作を処理するときにスレッドの安全性を制御する、より適切な方法と見なされます...

(リモートデバッグが失敗した場合)Webアプリをデバッグするには、次を使用します(「debug.txt」はサーバーの\ binフォルダーにあります)。

public static class LoggingExtensions
{
    static ReaderWriterLock locker = new ReaderWriterLock();
    public static void WriteDebug(string text)
    {
        try
        {
            locker.AcquireWriterLock(int.MaxValue); 
            System.IO.File.AppendAllLines(Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).Replace("file:\\", ""), "debug.txt"), new[] { text });
        }
        finally
        {
            locker.ReleaseWriterLock();
        }
    }
}

これで時間を節約できることを願っています。

18