【问题标题】:Concurrent read/write access to XML对 XML 的并发读/写访问
【发布时间】:2018-12-17 05:55:06
【问题描述】:

我正在尝试通过在多台机器上同时运行的多个进程在 XML 文件中实现信息更新。我的想法是循环 10 分钟,尝试打开和锁定文件以随机间隔最多 1 秒写入。文件打开并锁定后,我会加载所有 XML,添加当前机器的信息,对 XML 进行排序,然后转售并移除锁定,以便下一台机器可以打开。问题是 Get-Content 不会锁定文件,因此两台机器可以加载相同的 XML,而不是第二台机器使用来自第一台的数据加载 XML。 我找到了this,它提供了一种锁定文件的方法,然后通过流读取,但是当我尝试修改为这个时

$file = [IO.File]::Open($path, 'Open', 'ReadWrite', 'None')
$xml = Get-Content $path

我收到一个错误,因为文件被锁定。似乎 Get-Content 没有锁定文件,但它确实尊重已经存在的锁。 那么,有没有办法锁定文件,只有机器锁定才能读写? 也许更重要的是,这是否是正确的方法,还是有其他方法可以访问多个 XML?这似乎是一种常见的情况,因此必须有一些最佳实践方法来做到这一点,即使没有本机 cmdlet 方法。 FWIW,我必须支持回到 PowerShell 2.0,这无疑限制了我如何解决这个问题。

编辑:好吧,[io.file] 位中的第三个参数的 Read 似乎不起作用。 我现在有了这个

$path = '\\Px\Support\Px Tools\Resources\jobs.xml'
foreach ($i in 1..10) {
    $sleepTime = get-random -minimum:2 -maximum:5
    $file = [IO.File]::Open($path, 'Open', 'ReadWrite', 'Read')
    [xml]$xml = Get-Content $path

    $newNode = $xml.createElement('Item')
    $newNode.InnerXml = "$id : $i : $sleepTime : $(Get-Date)"
    $xml.DocumentElement.AppendChild($newNode) > $null
    $xml.Save($path)
    $file.Close()
}

理论上应该采用我拥有的 XML,带有两个虚拟日志项,读取它,附加另一个日志项(带有 ID、迭代、睡眠时间和时间戳)并重复 10 次,随机睡眠之间。它会在床上大便,试图用

保存
"The process cannot access the file '\\Px\Support\Px Tools\Resources\jobs.xml' because it is being used by another process."

我真的在尝试做之前没有做过 1000 次的事情吗?

好的,根据 cmets,这就是我所在的位置。我想确保在处理过程中不能(轻松)手动编辑原件。所以我已经实现了这个。 1:查找哨兵文件,如果没有找到 2:锁定原始文件,使其无法修改 3:复制原件为哨兵文件 4:根据需要修改sentinel文件 5:解锁原版 6:将哨兵文件复制到原始文件上 7:删除哨兵

在我看来,不确定的一点是如果有人在解锁它和哨兵被复制之间手动修改了原件,这是极不可能的。但是,似乎应该有一种方法可以 100% 确定地处理这个问题,我想不出有没有哨兵文件的方法。

【问题讨论】:

  • 下面的答案现在向您展示了如何在没有哨兵文件的情况下做到这一点。您描述的哨兵文件方法否定了使用哨兵文件的好处 - 请使用概述的算法@ 987654322@,关键部分是如何声明哨兵文件以避免竞争条件,并且在新的之前不获得排他锁内容已准备好替换文件。
  • 问题是文件被锁定后才能准备新内容。我正在读取文件的当前内容,添加,排序,然后保存。所以我需要先锁定然后再阅读,所以我可以确定,当我添加新信息时,不会添加任何其他信息。我有一些似乎正在工作的东西。是时候查看下面的信息了,看看这些信息如何影响我所拥有的任何变化。
  • 是的,如果您使用所有进程都尊重的哨兵文件,您可以将准备工作分开,如链接答案中所述 - 这是使用哨兵文件。如果您不介意在准备任务期间也专门锁定您的文件,那么下面的答案应该可以。

标签: xml powershell concurrency


【解决方案1】:

一般来说:文件并没有像数据库那样针对并发访问进行优化,所以如果您需要一些复杂的并发访问,您需要自己动手。

This answer 到一个密切相关的问题演示了使用单独的锁定文件(哨兵文件)来管理与 的并发性最小的干扰

但是,您可以简化该方法并避免对锁定文件的需要,如果您愿意为阅读、修改和保存修改的整个持续时间

相比之下,锁文件方法允许在其他进程读取文件的同时读取和准备修改,并且只需要独占锁来执行重写/替换文件的实际操作。

但是,对于这两种方法,都需要对文件进行一段时间的排他性锁定,以防止读取文件在重写时读取文件的不可预测性。

也就是说,您仍然需要所有相关流程的合作

  • 写入者需要处理(暂时)无法以独占方式打开文件,即当其他进程(读取者或写入者)正在使用它时。

  • 同样,读者必须准备好处理(暂时)无法打开文件(在作者更新文件时)。

关键是:

  • 使用文件共享模式None 打开文件(即,在您打开文件时拒绝其他进程使用同一文件),并保持打开状态直到更新完成。这确保了操作从跨进程的角度来看是原子的。

  • 仅使用 [System.IO.File]::Open() 返回的 FileStream 实例来读取和写入文件(调用 cmdlet 或 .NET 方法,例如 System.Xml.XmlDocument.Save()失败,因为他们自己会尝试打开 - 然后以独占方式锁定的 - 文件)。


这是实现排他锁定的代码的固定版本:

$path = '\\Px\Support\Px Tools\Resources\jobs.xml'
foreach ($i in 1..10) {

    $sleepTime = get-random -minimum:2 -maximum:5

    # Open the file with an exclusive lock so that no other process will be
    # be able to even read it while an update is being performed.
    # Use a RETRY LOOP until exclusive locking succeeds.
    # You'll need a similar loop for *readers*.
    # Note: In production code, you should also implement a TIMEOUT.
    do {  # retry loop
      try {
        $file = [IO.File]::Open($path, 'Open', 'ReadWrite', 'None')
      } catch {
        # Did opening fail due to the file being LOCKED? -> keep trying.
        if ($_.Exception.InnerException -is [System.IO.IOException] -and ($_.Exception.InnerException.HResult -band 0x21) -in 0x21, 0x20) { 
          $host.ui.Write('.') # Some visual feedback
          Start-Sleep -Milliseconds 500 # Sleep a little.
          continue # Try again.
        }
        Throw # Unexpexted error -> rethrow.
      }
      break # Opening with exclusive lock succeeded, proceed below.
    } while ($true)


    # Read the file's content into an XML document (DOM).
    $xml = New-Object xml # xml is a type accelerator for System.XML.XMLDocument
    $xml.Load($file)

    # Modify the XML document.
    $newNode = $xml.createElement('Item')
    $newNode.InnerXml = "$id : $i : $sleepTime : $(Get-Date)"
    $null = $xml.DocumentElement.AppendChild($newNode)

    # Convert the XML document back to a string
    # and write that string back to the file.
    $file.SetLength(0) # truncate existing content first
    $xml.Save($file)

    # Close the file and release the lock.
    $file.Close()
}

至于你尝试了什么

$file = [IO.File]::Open($path, 'Open', 'ReadWrite', 'Read') 以允许其他进程读取访问但不允许写入访问的方式打开文件。

然后您在 $file 仍处于打开状态时调用 $xml.Save($path),但该方法调用 - 它本身也尝试打开文件 - 需要 write 访问权限,但失败了。

如上图,关键是要使用同一个$fileFileStream实例专门用来打开文件,用于更新文件。

另请注意,在$xml.Save($path) 之前调用$file.Close() 不是解决方案,因为这会引入竞争条件,另一个进程可能会在之间的时间内打开文件> 这两种说法。

【讨论】:

  • 我认为 $xml.Load($file) 是缺失的部分。我一直试图从文件本身加载 XML,而不是文件变量。我还必须使用 [xml]$xml = New-Object System.XML.XMLDocument 来让它在 PS 2.0 中工作。但我只是同时运行了 3 个 VM 的测试,每个 VM 产生 20 个作业,每个作业执行 20 次写入。看起来一切正常,所以我要去参加比赛了。谢谢!
  • @Gordon;谢谢 - 忘记了 PSv2 兼容性;使用New-Object xml 更新答案,利用System.Xml.XmlDocument 的类型加速器。是的,only 使用独占锁定的$file 文件流对象是使解决方案工作的关键,.Load().Save() 都适用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多