.NETでファイルがロック解除されるまで待ちます


103

ファイルのロックが解除され、ファイルの読み取りと名前の変更が可能になるまでスレッドをブロックする最も簡単な方法は何ですか?たとえば、.NET FrameworkのどこかにWaitOnFile()がありますか?

FileSystemWatcherを使用してFTPサイトに送信されるファイルを検索するサービスがありますが、ファイル作成イベントは、他のプロセスがファイルの書き込みを完了する前に発生します。

理想的なソリューションにはタイムアウト期間があり、そのためスレッドはあきらめる前に永遠にハングアップしません。

編集:以下のソリューションのいくつかを試した後、すべてのファイルがに書き込まれるようにシステムを変更し、最終的な場所にをPath.GetTempFileName()実行しFile.Move()ました。FileSystemWatcherイベントが発生するとすぐに、ファイルはすでに完成しています。


4
.NET 4.0のリリース以降、この問題を解決するより良い方法はありますか?
ジェイソン2010年

回答:


40

これは私が関連する質問に与えた答えでした:

    /// <summary>
    /// Blocks until the file is not locked any more.
    /// </summary>
    /// <param name="fullPath"></param>
    bool WaitForFile(string fullPath)
    {
        int numTries = 0;
        while (true)
        {
            ++numTries;
            try
            {
                // Attempt to open the file exclusively.
                using (FileStream fs = new FileStream(fullPath,
                    FileMode.Open, FileAccess.ReadWrite, 
                    FileShare.None, 100))
                {
                    fs.ReadByte();

                    // If we got this far the file is ready
                    break;
                }
            }
            catch (Exception ex)
            {
                Log.LogWarning(
                   "WaitForFile {0} failed to get an exclusive lock: {1}", 
                    fullPath, ex.ToString());

                if (numTries > 10)
                {
                    Log.LogWarning(
                        "WaitForFile {0} giving up after 10 tries", 
                        fullPath);
                    return false;
                }

                // Wait for the lock to be released
                System.Threading.Thread.Sleep(500);
            }
        }

        Log.LogTrace("WaitForFile {0} returning true after {1} tries",
            fullPath, numTries);
        return true;
    }

7
私はこれは醜いが唯一の可能な解決策を見つける
knoopx 2009

6
これは本当に一般的なケースでうまくいくでしょうか?using()句でファイルを開くと、usingスコープの終了時にファイルが閉じられ、ロックが解除されます。これと同じ戦略を使用する2番目のプロセスがある場合(繰り返し再試行)、WaitForFile()の終了後、ファイルを開くことができるかどうかに関して競合状態が発生します。番号?
Cheeso

74
悪いアイデア!概念は正しいですが、より良い解決策はブールではなくFileStreamを返すことです。ユーザーがファイルをロックする機会が得られる前にファイルが再度ロックされた場合
Nissim

2
フェロの方法はどこですか?
Vbp

1
Nissimのコメントも私が考えていたものとまったく同じですが、シークを使用する場合は、バイトを読み取った後に必ず0にリセットしてください。 fs.Seek(0、SeekOrigin.Begin);
2015

73

エリックの答えから始めて、コードをよりコンパクトで再利用可能にするためにいくつかの改善を加えました。お役に立てれば幸いです。

FileStream WaitForFile (string fullPath, FileMode mode, FileAccess access, FileShare share)
{
    for (int numTries = 0; numTries < 10; numTries++) {
        FileStream fs = null;
        try {
            fs = new FileStream (fullPath, mode, access, share);
            return fs;
        }
        catch (IOException) {
            if (fs != null) {
                fs.Dispose ();
            }
            Thread.Sleep (50);
        }
    }

    return null;
}

16
このコードは今でも魅力のように機能すると言います。ありがとう。
小野仙台2013

6
@PabloCostaまさに!それ閉じることはできません。閉じた場合、別のスレッドが競合して開き、目的を無効にする可能性があるためです。開いたままにしておくため、この実装は正しいです。呼び出し側にそれについて心配させてusingください。ヌルで安全usingです。ブロック内でヌルをチェックするだけです。
doug65536

2
「FileStream fs = null;」tryの外側でforの内側で宣言する必要があります。次に、try内でfsを割り当てて使用します。catchブロックは、「if(fs!= null)fs.Dispose();」を実行する必要があります。(またはC#6のfs?.Dispose()のみ)、返されないFileStreamが適切にクリーンアップされるようにします。
Bill Menees

1
バイトを読み取ることが本当に必要ですか?私の経験では、ファイルを読み取りアクセス用に開いた場合、それを持っているので、テストする必要はありません。ここでの設計では、排他的アクセスを強制しているわけではありませんが、最初のバイトを読み取ることはできても、他のバイトを読み取ることができない場合もあります(バイトレベルのロック)。元の質問から、読み取り専用の共有レベルで開く可能性が高いため、他のプロセスはファイルをロックまたは変更できません。とにかく、使用方法によっては、fs.ReadByte()は完全に無駄になるか、十分ではないと感じます。
eselk 2017

8
ブロックでfsnullにできない状況はどのユーザーcatchですか?場合はFileStream、コンストラクタは、スロー、変数に値を割り当てられないだろう、と内部の他には何もありませんtry投げることができますIOException。私にはそれだけで大丈夫なようですreturn new FileStream(...)
Matti Virkkunen 2017年

18

これは、ファイル操作自体から独立した、これを行うための一般的なコードです。これはそれを使用する方法の例です:

WrapSharingViolations(() => File.Delete(myFile));

または

WrapSharingViolations(() => File.Copy(mySourceFile, myDestFile));

再試行回数、および再試行間の待機時間も定義できます。

注:残念ながら、基になるWin32エラー(ERROR_SHARING_VIOLATION)は.NETで公開されていないためIsSharingViolation、リフレクションメカニズムに基づく小さなハッキング関数()を追加して、これを確認しています。

    /// <summary>
    /// Wraps sharing violations that could occur on a file IO operation.
    /// </summary>
    /// <param name="action">The action to execute. May not be null.</param>
    public static void WrapSharingViolations(WrapSharingViolationsCallback action)
    {
        WrapSharingViolations(action, null, 10, 100);
    }

    /// <summary>
    /// Wraps sharing violations that could occur on a file IO operation.
    /// </summary>
    /// <param name="action">The action to execute. May not be null.</param>
    /// <param name="exceptionsCallback">The exceptions callback. May be null.</param>
    /// <param name="retryCount">The retry count.</param>
    /// <param name="waitTime">The wait time in milliseconds.</param>
    public static void WrapSharingViolations(WrapSharingViolationsCallback action, WrapSharingViolationsExceptionsCallback exceptionsCallback, int retryCount, int waitTime)
    {
        if (action == null)
            throw new ArgumentNullException("action");

        for (int i = 0; i < retryCount; i++)
        {
            try
            {
                action();
                return;
            }
            catch (IOException ioe)
            {
                if ((IsSharingViolation(ioe)) && (i < (retryCount - 1)))
                {
                    bool wait = true;
                    if (exceptionsCallback != null)
                    {
                        wait = exceptionsCallback(ioe, i, retryCount, waitTime);
                    }
                    if (wait)
                    {
                        System.Threading.Thread.Sleep(waitTime);
                    }
                }
                else
                {
                    throw;
                }
            }
        }
    }

    /// <summary>
    /// Defines a sharing violation wrapper delegate.
    /// </summary>
    public delegate void WrapSharingViolationsCallback();

    /// <summary>
    /// Defines a sharing violation wrapper delegate for handling exception.
    /// </summary>
    public delegate bool WrapSharingViolationsExceptionsCallback(IOException ioe, int retry, int retryCount, int waitTime);

    /// <summary>
    /// Determines whether the specified exception is a sharing violation exception.
    /// </summary>
    /// <param name="exception">The exception. May not be null.</param>
    /// <returns>
    ///     <c>true</c> if the specified exception is a sharing violation exception; otherwise, <c>false</c>.
    /// </returns>
    public static bool IsSharingViolation(IOException exception)
    {
        if (exception == null)
            throw new ArgumentNullException("exception");

        int hr = GetHResult(exception, 0);
        return (hr == -2147024864); // 0x80070020 ERROR_SHARING_VIOLATION

    }

    /// <summary>
    /// Gets the HRESULT of the specified exception.
    /// </summary>
    /// <param name="exception">The exception to test. May not be null.</param>
    /// <param name="defaultValue">The default value in case of an error.</param>
    /// <returns>The HRESULT value.</returns>
    public static int GetHResult(IOException exception, int defaultValue)
    {
        if (exception == null)
            throw new ArgumentNullException("exception");

        try
        {
            const string name = "HResult";
            PropertyInfo pi = exception.GetType().GetProperty(name, BindingFlags.NonPublic | BindingFlags.Instance); // CLR2
            if (pi == null)
            {
                pi = exception.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance); // CLR4
            }
            if (pi != null)
                return (int)pi.GetValue(exception, null);
        }
        catch
        {
        }
        return defaultValue;
    }

5
彼らは本当に提供することができましたSharingViolationException。実際、から派生している限り、下位互換性を維持できIOExceptionます。そして、彼らは本当に、本当にそうすべきです。
ロマン・スターコフ


9
.NET Framework 4.5、.NET Standard、および.NET Coreでは、HResultはExceptionクラスのパブリックプロパティです。これには反射は必要ありません。MSDNから:Starting with the .NET Framework 4.5, the HResult property's setter is protected, whereas its getter is public. In previous versions of the .NET Framework, both getter and setter are protected.
NightOwl888

13

私はこれらの種類のヘルパークラスを一緒に投げました。ファイルにアクセスするすべてのものを制御できる場合に機能します。他の多くのものか​​らの競合を予想している場合、これはかなり価値がありません。

using System;
using System.IO;
using System.Threading;

/// <summary>
/// This is a wrapper aroung a FileStream.  While it is not a Stream itself, it can be cast to
/// one (keep in mind that this might throw an exception).
/// </summary>
public class SafeFileStream: IDisposable
{
    #region Private Members
    private Mutex m_mutex;
    private Stream m_stream;
    private string m_path;
    private FileMode m_fileMode;
    private FileAccess m_fileAccess;
    private FileShare m_fileShare;
    #endregion//Private Members

    #region Constructors
    public SafeFileStream(string path, FileMode mode, FileAccess access, FileShare share)
    {
        m_mutex = new Mutex(false, String.Format("Global\\{0}", path.Replace('\\', '/')));
        m_path = path;
        m_fileMode = mode;
        m_fileAccess = access;
        m_fileShare = share;
    }
    #endregion//Constructors

    #region Properties
    public Stream UnderlyingStream
    {
        get
        {
            if (!IsOpen)
                throw new InvalidOperationException("The underlying stream does not exist - try opening this stream.");
            return m_stream;
        }
    }

    public bool IsOpen
    {
        get { return m_stream != null; }
    }
    #endregion//Properties

    #region Functions
    /// <summary>
    /// Opens the stream when it is not locked.  If the file is locked, then
    /// </summary>
    public void Open()
    {
        if (m_stream != null)
            throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage);
        m_mutex.WaitOne();
        m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare);
    }

    public bool TryOpen(TimeSpan span)
    {
        if (m_stream != null)
            throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage);
        if (m_mutex.WaitOne(span))
        {
            m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare);
            return true;
        }
        else
            return false;
    }

    public void Close()
    {
        if (m_stream != null)
        {
            m_stream.Close();
            m_stream = null;
            m_mutex.ReleaseMutex();
        }
    }

    public void Dispose()
    {
        Close();
        GC.SuppressFinalize(this);
    }

    public static explicit operator Stream(SafeFileStream sfs)
    {
        return sfs.UnderlyingStream;
    }
    #endregion//Functions
}

名前付きミューテックスを使用して動作します。ファイルへのアクセスを希望するユーザーは、ファイルの名前を共有する名前付きミューテックスの制御を獲得しようとします( '\'が '/'に変わります)。ミューテックスがアクセス可能になるまで停止するOpen()を使用するか、指定された期間ミューテックスを取得しようとし、期間内に取得できない場合はfalseを返すTryOpen(TimeSpan)を使用できます。これは、ロックが適切に解放され、このオブジェクトが破棄されるときにストリーム(開いている場合)が適切に破棄されるようにするために、usingブロック内で使用される可能性があります。

私はファイルのさまざまな読み取り/書き込みを行うために〜20のことで簡単なテストを行いましたが、破損は見られませんでした。明らかにそれはそれほど高度ではありませんが、単純なケースの大部分で機能するはずです。


5

この特定のアプリケーションでは、ファイルを直接監視すると、特にファイルサイズが大きくなると、トレースが困難になるバグが必然的に発生します。機能する2つの異なる戦略を次に示します。

  • 2つのファイルをFTPするが、1つだけを監視する。たとえば、ファイルimportant.txtとimportant.finishを送信します。終了ファイルのみを監視し、txtを処理します。
  • 1つのファイルをFTPしますが、完了したら名前を変更します。たとえば、important.waitを送信し、完了したら送信者にそれをimportant.txtに名前変更してもらいます。

幸運を!


これは自動の反対です。これは、手動でファイルを取得するようなもので、手順が増えます。
HackSlash

4

昔使用したテクニックの1つは、独自の関数を記述することでした。基本的に、例外をキャッチし、指定した期間起動できるタイマーを使用して再試行します。より良い方法がある場合は、共有してください。


3

MSDNから:

OnCreatedイベントは、ファイルが作成されるとすぐに発生します。ファイルが監視ディレクトリにコピーまたは転送されている場合、OnCreatedイベントがすぐに発生し、その後に1つ以上のOnChangedイベントが発生します。

FileSystemWatcherを変更して、 "OnCreated"イベント中に読み取り/名前変更を行わないようにすることができます。

  1. (FileInfoオブジェクトを使用して)ロックされなくなるまでファイルステータスをポーリングするスレッドをスパンします。
  2. ファイルがロックされておらず、準備ができていると判断するとすぐに、サービスを呼び出してファイルを処理します。

1
filesystemwatcherのスレッドを生成すると、基盤となるバッファーがオーバーフローし、変更された多くのファイルが失われる可能性があります。より良いアプローチは、コンシューマー/プロデューサーキューを作成することです。
Nissim 2010

2

ほとんどの場合、@ harpoのような単純なアプローチが機能します。このアプローチを使用して、より洗練されたコードを開発できます。

  • SystemHandleInformation \ SystemProcessInformationを使用して、選択したファイルの開いているハンドルをすべて検索します
  • その内部ハンドルにアクセスするためのWaitHandleクラスのサブクラス
  • サブクラス化されたWaitHandleでラップされた検出されたハンドルをWaitHandle.WaitAnyメソッドに渡す

2

転送プロセストリガーファイルSameNameASTrasferedFile.trgを転送する広告は、ファイルの送信が完了した後に作成されます。

次に、*。trgファイルでのみイベントを発生させるFileSystemWatcherをセットアップします。


1

ファイルのロックステータスを判別するために何を使用しているかはわかりませんが、このようなことで判別できます。

while(true)
{
    {を試す
        stream = File.Open(fileName、fileMode);
        ブレーク;
    }
    catch(FileIOException){

        //ロックの問題かどうかを確認します

        Thread.Sleep(100);
    }
}

1
少し遅れますが、ファイルが何らかの理由でロックされていると、ループを終了することはできません。カウンターを追加する必要があります(最初の回答を参照)。
ピーター

0

可能な解決策は、filesystemwatcherをいくつかのポーリングと組み合わせることです。

ファイルの変更ごとに通知を受け取り、通知を受けたときに、現在受け入れられている回答に記載されているように、ファイルがロックされているかどうかを確認します。 ます。https //stackoverflow.com/a/50800/6754146ファイルストリームを開くためのコードは回答からコピーされます少し変更しました:

public static void CheckFileLock(string directory, string filename, Func<Task> callBack)
{
    var watcher = new FileSystemWatcher(directory, filename);
    FileSystemEventHandler check = 
        async (sender, eArgs) =>
    {
        string fullPath = Path.Combine(directory, filename);
        try
        {
            // Attempt to open the file exclusively.
            using (FileStream fs = new FileStream(fullPath,
                    FileMode.Open, FileAccess.ReadWrite,
                    FileShare.None, 100))
            {
                fs.ReadByte();
                watcher.EnableRaisingEvents = false;
                // If we got this far the file is ready
            }
            watcher.Dispose();
            await callBack();
        }
        catch (IOException) { }
    };
    watcher.NotifyFilter = NotifyFilters.LastWrite;
    watcher.IncludeSubdirectories = false;
    watcher.EnableRaisingEvents = true;
    //Attach the checking to the changed method, 
    //on every change it gets checked once
    watcher.Changed += check;
    //Initially do a check for the case it is already released
    check(null, null);
}

このようにして、ファイルがロックされているかどうかを確認し、指定されたコールバックに対して閉じられたときに通知を受け取ることができます。これにより、過度に積極的なポーリングを回避し、実際に閉じられている可能性があるときにのみ作業を行うことができます。


-1

私はそれをGulzarと同じ方法で行います。ループで試してください。

実際、私はファイルシステムウォッチャーについてさえ気にしません。1分に1回ネットワークドライブをポーリングして新しいファイルを探すのは簡単です。


2
安いかもしれませんが、1分に1回は多くのアプリケーションにとって長すぎます。リアルタイムの監視が不可欠な場合があります。C#でファイルシステムメッセージをリッスンするものを実装する必要がある代わりに(これらのものにとって最も便利な言語ではありません)、FSWを使用します。
ThunderGr 2013年

-1

NotifyFilter NotifyFilters.LastWriteでChangedイベントを使用するだけです

var watcher = new FileSystemWatcher {
      Path = @"c:\temp\test",
      Filter = "*.xml",
      NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed; 
watcher.EnableRaisingEvents = true;

1
FileSystemWatcherは、ファイルへの書き込みが完了したときに通知するだけではありません。これは、「単一の」論理書き込みについて何度も通知します。最初の通知を受け取った後でファイルを開こうとすると、例外が発生します。
ロス

-1

Outlookの添付ファイルを追加すると、同様の問題が発生しました。「使用」はその日を救った。

string fileName = MessagingBLL.BuildPropertyAttachmentFileName(currProp);

                //create a temporary file to send as the attachment
                string pathString = Path.Combine(Path.GetTempPath(), fileName);

                //dirty trick to make sure locks are released on the file.
                using (System.IO.File.Create(pathString)) { }

                mailItem.Subject = MessagingBLL.PropertyAttachmentSubject;
                mailItem.Attachments.Add(pathString, Outlook.OlAttachmentType.olByValue, Type.Missing, Type.Missing);

-3

オプションとしてこれはどうですか:

private void WaitOnFile(string fileName)
{
    FileInfo fileInfo = new FileInfo(fileName);
    for (long size = -1; size != fileInfo.Length; fileInfo.Refresh())
    {
        size = fileInfo.Length;
        System.Threading.Thread.Sleep(1000);
    }
}

もちろん、ファイルサイズが作成時に事前に割り当てられている場合は、誤検知が発生します。


1
ファイルへの書き込みプロセスが1秒以上停止するか、メモリ内のバッファが1秒以上停止すると、別の誤検知が発生します。これはどんな状況でも良い解決策だとは思いません。
クリスウェナム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.