【问题标题】:FileSystemWatcher cannot access files because used in other processFileSystemWatcher 无法访问文件,因为在其他进程中使用
【发布时间】:2017-11-16 13:05:15
【问题描述】:

我正在编写一个 FileSystemWatcher,每当图像上传到文件夹 A 时,它将图像从文件夹 A 复制到文件夹 B。我试图将其用作服务器 PC 上的 Windows 服务,但我有我的文件在复制时被锁定的一些问题。我想我已经找到了问题的根源,但我没有任何运气解决它。因此,当我运行我的 Windows 服务时,它总是在第一张或第二张图片上传时意外结束。我收到的错误消息是这样的:The process cannot access the file 'filepath' because it is being used by another process.

我的代码的相关部分:

public void WatchForChanges()
{
    FileSystemWatcher watcher = new FileSystemWatcher();
    watcher.Path = Program.SourceFolder;
    watcher.Created += new FileSystemEventHandler(OnImageAdded);
    watcher.EnableRaisingEvents = true;
    watcher.IncludeSubdirectories = true;
}

public void OnImageAdded(object source, FileSystemEventArgs e)
{
    FileInfo file = new FileInfo(e.FullPath);
    ImageHandler handler = new ImageHandler();
    if (handler.IsImage(file))
    {
        handler.CopyImage(file);
    }
}

还有我的 CopyImage 方法,其中包括我针对此问题提出的解决方案之一,它利用了一个 while 循环来捕获错误并重试图像的复制:

public void CopyImage(FileSystemInfo file)
{
    // code that sets folder paths
    // code that sets folder paths

    bool retry = true;
    if (!Directory.Exists(targetFolderPath))
    {
        Directory.CreateDirectory(targetFolderPath);
    }
    while (retry)
    {
        try
        {
            File.Copy(file.FullName, targetPath, true);
            retry = false;
        }
        catch (Exception e)
        {
            Thread.Sleep(2000);
        }
    }
}

但是这个CopyImage 解决方案只是继续复制同一个文件,这对我来说不是很理想。我希望这已经足够了,但遗憾的是,我有一排图像在等待。

【问题讨论】:

  • 而不是使用while (retry) 将失败的副本写入HashSet<string>。定期循环 HashSet 并重试。
  • 相关文件可能仍在写入过程中。也许您应该使用队列,类似于@mjwills 的建议。

标签: c# filesystemwatcher


【解决方案1】:

图像文件可能是由另一个应用程序创建的,该应用程序在读取和写入外部进程时使用了独占访问锁(有关更多信息,请阅读this,尤其是与 Microsoft Windows 相关的段落)。您必须:

  • 停止/杀死正在使用该文件的进程;
  • 等到文件不再被使用。

由于其他进程可能在您尝试使用应用程序复制文件时正在写入文件,因此第一个选项绝不推荐。它也可能是检查新文件的防病毒软件,即使在这种情况下,也不推荐第一个选项。

您可以尝试将以下代码集成到您的 CopyImage 方法中,以便您的应用程序将等到文件不再使用后再继续:

private Boolean WaitForFile(String filePath)
{
    Int32 tries = 0;

    while (true)
    {
        ++tries;

        Boolean wait = false;
        FileStream stream = null;

        try
        {
            stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None);
            break;
        }
        catch (Exception ex)
        {
            Logger.LogWarning("CopyImage({0}) failed to get an exclusive lock: {1}", filePath, ex.ToString());

            if (tries > 10)
            {
                Logger.LogWarning("CopyImage({0}) skipped the file after 10 tries.", filePath);
                return false;
            }

            wait = true;
        }
        finally
        {
            if (stream != null)
                stream.Close();
        }

        if (wait)
            Thread.Sleep(250);
    }

    Logger.LogWarning("CopyImage({0}) got an exclusive lock after {1} tries.", filePath, tries);

    return true;
}

【讨论】:

  • 太好了,感谢您的帖子。你的意思是我应该在复制之前使用 WaitForFile 作为检查 - 例如:if (WaitForFile(targetPath)) { File.Copy(file.FullName, targetPath, true); } ?
  • 您可以将其用作独立检查,也可以将其集成(合并)到更高级别的 CopyImage 方法中。
【解决方案2】:

虽然看起来很简单,但它确实复杂得令人不满意。

问题是,当您收到通知时,正在写入文件的应用程序并没有完成它......因此您遇到了并发问题。没有很好的方法可以知道文件何时关闭。嗯..一种方法是订阅日志事件 - 这就是 FileSystemWatcher 所做的 - 但这是相当复杂的,并且需要 lot 的移动部件。走这条路线,您可以在文件关闭时收到通知。如果您有兴趣,请参阅https://msdn.microsoft.com/en-us/library/windows/desktop/aa363798(v=vs.85).aspx。

我会将工作分为两部分。我想我会启动一个ThreadPool 线程来完成这项工作,并让它从FileSystemWatcher's 事件处理程序写入的列表中读取它的工作。这样,事件处理程序会快速返回。 ThreadPool 线程将遍历它的列表,试图在文件上获得一个排他锁(类似于 Tommaso 的代码)。如果它不能,它只是移动到下一个文件。每次成功复制时,它都会从列表中删除该文件。

您需要关注线程安全...因此您需要创建一个静态对象来协调对列表的写入。事件处理程序和ThreadPool 线程都会在写入时持有锁。

这是整个方法的一个脚手架:

internal sealed class Copier: IDisposable
{
  static object sync = new object();
  bool quit;
  FileSystemWatcher watcher;
  List<string> work;

  internal Copier( string pathToWatch )
  {
    work = new List<string>();
    watcher = new FileSystemWatcher();
    watcher.Path = pathToWatch;
    watcher.Create += QueueWork;
    ThreadPool.QueueUserWorkItem( TryCopy );
  }

  void Dispose()
  {
    lock( sync ) quit = true;
  }

  void QueueWork( object source, FileSystemEventArgs args )
  {
    lock ( sync )
    {
      work.Add( args.FullPath );
    }
  }

  void TryCopy( object args )
  {
    List<string> localWork;
    while( true )
    {
      lock ( sync ) 
      { 
        if ( quit ) return;  //--> we've been disposed
        localWork = new List<string>( work );
      }
      foreach( var fileName in localWork )
      {
        var locked = true;
        try
        {
          using 
          ( var throwAway = new FileStream
            ( fileName, 
              FileMode.Open,
              FileAccess.Read,
              FileShare.None
            )
          ); //--> no-op  - will throw if we can't get exclusive read        
          locked = false;
        }
        catch { }
        if (!locked )
        {
          File.Copy( fileName, ... );
          lock( sync ) work.Remove( fileName );
        }
      }
    }
  }
}

未测试 - 在答案中写在这里...但是它或类似的东西将涵盖基础。

【讨论】:

  • 考虑使用线程安全的结构(例如ConcurrentQueue / BlockingCollection)来避免显式锁定的需要。
  • 这个答案的一个变体让它工作了——非常感谢你以正确的方式推动我!!
  • @mjwills,好点子。我仍然必须生活在 3.5 土地上,并且经常想老办法。
猜你喜欢
  • 2012-08-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多