c#アプリケーションで発生した予期しない共用違反について

# 1. 概要
最近、自作の c# アプリケーションでファイルの共用違反 (Sharing Violation) が発生しました。
3日ほど調査を試みたのですが、結局、原因はおろか発生条件すら不明のままです。
しかし、問題の回避方法らしきものはわかったので、この場で報告しようと思います。
本稿が同じような問題でお悩みの方の一助となれば幸いです。

# 2. 実行環境
- OS
Windows 10 64bit Version 22H2 (OS build 19045,5854)
- .NET Runtime
  - .NET 8.0.16
  - .NET 9.0.5

# 3. 発生した現象
自作のライブラリのテストプログラムを実行中に、ファイルの共用違反が発生しました。
1000 回以上の同じような処理の繰り返しの最中に数回だけ発生しましたので、**再現頻度は稀**と言っていいでしょう。

問題が発生したテストプログラムおよび自作ライブラリのソースコードは膨大な量になるため、処理の流れを説明するために[ファイルアクセス部分のみを抜き出したソースコード](#8-%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89)を書いてみました。しかし**残念ながらこのソースコードそのものをビルドして実行しても問題は再現しません。**

このソースコードでは以下のような処理を繰り返しています。

```c#
var outStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
// ここで outStream へデータの書き込み
outStream.Dispose();

// 以下の new FileStream() で IOException 例外の可能性
var inStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None);
// ここで inStream からデータの読み込み
inStream.Dispose();

// 以下の File.Delete() で IOException 例外の可能性
File.Delete(path);
```

例外のエラーメッセージは以下の通りで、例外オブジェクトの) `HResult` プロパティの値は `0x80070020` (`-2147024864`) です。これは Win32 API のエラーコードの `ERROR_SHARING_VIOLATION` (`32`) に相当します。

> The process cannot access the file '*file] path name*' because it is being used by another process.

# 4. 調査結果
前述したように、正確な発生条件も原因も不明ですが、調査した内容をまとめておきます。

- 当該ファイルはテストプログラム専用のファイルであり、そのファイルにアクセスしている他のアプリケーションは存在しないはずである。
- .net8.0 および .net9.0 で問題の発生を確認済みである。
- Release ビルドおよび Debug ビルドで問題の発生を確認済みである。
- 例外が発生した場合でも、`Stream` オブジェクトに対する `Dispose()` または `Close()` の漏れは存在しない。(実行経過をトレースファイルに記録することにより確認)
- ファイルアクセスはすべてシングルスレッドで同期的に行われている。(実行経過をトレースファイルに記録することにより確認)
- Linux (ubuntu on WSL2) では問題が再現しない。

# 5. 考察

再現性が稀、ということから真っ先に想像できるのは、何らかのタイミングの問題か、メモリの初期化漏れでしょう。(少なくとも私の場合は)

しかし、メモリの初期化漏れは c# では非常に考えにくいです。また、すべてのファイルアクセスがシングルスレッドで同期的に行われていることから、タイミングの問題というのも考えにくいと思います。
.net ランタイムのファイルアクセス周りのソースコードも追ってみましたが、やはり非同期に実行されていたり、別スレッドで実行されている箇所は見つけることが出来ませんでした。

あと、試しにファイルの読み込みオープンの共用指定を `FileShare.None` (共用を許可しない) から `FileShare.Read` (読み込みの共用のみ許可する) に変更したところ、読み込みオープンの際には例外が発生しなくなりました。(ただしファイルの削除の際には相変わらず例外が発生することがありましたが)
よく考えるとこれは奇妙なことです。
何故ならば、直前に行われている同じファイルに対する書き込みオープンの共用指定が `FileShare.None` (共用を許可しない) であるので、仮に何らかの原因で書き込みオープンのクローズ漏れがあったとしても、その後の読み込みオープンは共用指定がどうであるかにかかわらず成功するはずがないからです。

~~ここまでの調査内容から考えた筆者の推測なのですが、Windows 固有の何かが悪さをしているせいで問題が発生しているような気がします。~~

# 6. 回避方法

考えつく限りの方法で調べても、正確な発生条件も原因も不明なので、半ば破れかぶれで **「エラーが発生したら成功するまで再試行する」** という身も蓋もない方法を試してみることにしました。
具体的には以下のようなコードになります。
```c#
private const int _E_ERROR_SHARING_VIOLATION = unchecked((int)0x80070020u);

// new FileStream(string, FileMode, FileAccess, FileShare) の代わりに使用する
private static FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share)
{
    for (var count = 0; ; ++count)
    {
        try
        {
            return new FileStream(path, mode, access, share);
        }
        catch (IOException ex)
        {
            if (!OperatingSystem.IsWindows() || ex.HResult != _E_ERROR_SHARING_VIOLATION || count >= 10)
                throw;
        }

        Thread.Sleep(100);
    }
}

// File.Delete(string) の代わりに使用する
private static void DeleteFile(string path)
{
    for (var count = 0; ; ++count)
    {
        try
        {
            File.Delete(path);
            return;
        }
        catch (IOException ex)
        {
            if (!OperatingSystem.IsWindows() || ex.HResult != _E_ERROR_SHARING_VIOLATION || count >= 10)
                throw;
        }

        Thread.Sleep(100);
    }
}
```
再試行の前に `Thread.Sleep()` を入れたのは CPU ループになるのが怖かったからです。再試行回数は適当に 100 回、再試行までの間のスリープ時間はこれも適当に 1ms にしました。

上記のコードを追加した上で、既存のソースコードのすべての `new FileStream()` および `File.Delete()` を `CreateFileStream()` および `DeleteFile()` に書き換えて実行したところ、問題は再現しなくなりました。

ちなみに、実行経過のトレースを採取して再試行回数がどれぐらいなのかを調べてみましたが、最大でも 1 回だけでした。
実は 100 回も再試行しなくても 1 回だけでもいいのかもしれませんが、そもそもの原因が不明なので、100 回のままにしてあります。

:::note info
##### [2025年6月14日 追記]
コメントにて、上記ソースコード中のスリープ時間と試行回数についてアドバイスをいただきました。

現時点でこれらの値は 1ms と 100 回にしています。
しかし、コメントで解説されているようなバックグラウンドで動作している何かとの競合の可能性を考えると、確かに現在の値では不適切に感じました。
そのため、上記ソースコード中のスリープ時間と再試行回数を 100ms と 10 回 に変更しました。

読者の方々が上記ソースコードを参考にされる場合、これらの値が想定される用途に合っていないようなら適切な値に変更してください。
:::

# 7. 結論
- Windows 上にて、`new FileStream()` または `File.Delete()` で稀に予期しないファイル共用違反例外 (`System.IO.IOExeption`)が発生することがある。
**メッセージ**: The process cannot access the file '*file] path name*' because it is being used by another process.
**HRESULT**: `0x80070020` (`-2147024864`) ※Win32 API エラーコードの `ERROR_SHARING_VIOLATION` (`32`) に相当
- 発生条件および原因は不明である。
- 回避方法については[6. 回避方法](#6-%E5%9B%9E%E9%81%BF%E6%96%B9%E6%B3%95)を参照されたい。

# 8. サンプルソースコード

元々の問題が発生したプログラムからファイルアクセス関連の処理をできるだけ忠実に抜き出したものが以下のソースコードになります。
残念ですが、**このソースコードでは問題が再現しませんでした。**
```c#
using System;
using System.IO;
using System.Text;

namespace Experiment
{
    internal sealed partial class Program
    {
        static void Main()
        {
            var path = Path.Combine(Environment.CurrentDirectory, "work.dat");
            var originalText1 = "<<Data Block 1>>";
            var originalText2 = "<<Data Block 2>>";
            var originalText3 = "<<Data Block 3>>";

            for (var count = 0; count < 1000; ++count)
                DoTest(path, originalText1, originalText2, originalText3);

            Console.Beep();
            Console.WriteLine("Complete");
            _ = Console.ReadLine();
        }

        private static void DoTest(string path, string originalText1, string originalText2, string originalText3)
        {
            var originalData1 = Encoding.UTF8.GetBytes(originalText1);
            var originalData2 = Encoding.UTF8.GetBytes(originalText2);
            var originalData3 = Encoding.UTF8.GetBytes(originalText3);

            Span<byte> buffer = stackalloc byte[256];
            try
            {
                using (var outStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
                {
                    var tempFilePath1 = Path.GetTempFileName();
                    try
                    {
                        using (var outTempStream1 = new FileStream(tempFilePath1, FileMode.Create, FileAccess.Write, FileShare.None))
                        {
                            var tempFilePath2 = Path.GetTempFileName();
                            try
                            {
                                using (var outTempStream2 = new FileStream(tempFilePath2, FileMode.Create, FileAccess.Write, FileShare.None))
                                {
                                    WriteData(outTempStream2, originalData1);
                                    outTempStream2.Flush();
                                }

                                using (var inTempStream2 = new FileStream(tempFilePath2, FileMode.Open, FileAccess.Read, FileShare.None))
                                {
                                    var length = ReadData(inTempStream2, buffer);
                                    if (!string.Equals(Encoding.UTF8.GetString(buffer[..length]), originalText1, StringComparison.Ordinal))
                                        throw new Exception();
                                }
                            }
                            finally
                            {
                                File.Delete(tempFilePath2);
                            }

                            WriteData(outTempStream1, originalData2);
                            outTempStream1.Flush();
                        }

                        using (var inTempStream1 = new FileStream(tempFilePath1, FileMode.Open, FileAccess.Read, FileShare.None))
                        {
                            var length = ReadData(inTempStream1, buffer);
                            if (!string.Equals(Encoding.UTF8.GetString(buffer[..length]), originalText2, StringComparison.Ordinal))
                                throw new Exception();
                        }

                        WriteData(outStream, originalData3);
                        outStream.Flush();
                    }
                    finally
                    {
                        File.Delete(tempFilePath1);
                    }
                }

                using (var inStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None)) // ここで共用違反例外の可能性
                {
                    var length = ReadData(inStream, buffer);
                    if (!string.Equals(Encoding.UTF8.GetString(buffer[..length]), originalText3, StringComparison.Ordinal))
                        throw new Exception();
                }
            }
            finally
            {
                File.Delete(path);  // ここで共用違反例外の可能性
            }
        }

        private static int ReadData(Stream inStream, Span<byte> buffer)
        {
            var totalLength = 0;
            while (buffer.Length > 0)
            {
                var length = inStream.Read(buffer);
                if (length <= 0)
                    return totalLength;
                buffer = buffer[length..];
                totalLength += length;
            }

            throw new Exception("The length of buffer is insufficient.");
        }

        private static void WriteData(Stream outStream, ReadOnlySpan<byte> buffer)
        {
            outStream.Write(buffer);
        }
    }
}
```