.NET (PowerShell) 并发文件使用(锁定,同时仍允许读取访问)

.NET (PowerShell) Concurrent file usage (locking, while still allowing read access)

我有一个 XML 文件需要同时访问。我不需要并发写入,但我需要并发读取。 reading/modifying 这个 XML 文件的指定方法是使用 PowerShell 脚本,该脚本使用 Linq to XML (XElement) 使用 XML 文件。

理想的场景是:

  1. 团队中的所有用户将在他们的工作日开始时 运行 此 PowerShell 脚本,并且它将保持 运行ning 直到工作日结束。
  2. 在整个工作日,团队中的任何用户都将输入预定义的命令来查询 XML 文件以收集数据。每次用户 运行 进行这些查询之一时,它将打开 XML 文件,执行查询,关闭 XML 文件,然后显示结果。
  3. 偶尔,在一天中,这些用户中的任何一个都需要修改 XML 文件。用户将输入一个预定义的命令,指定他们想要修改的节点,然后他们将输入他们想要的所有数据 add/modify。然后脚本将打开文件(锁定它,所以没有其他用户可以打开它 read/write),将 XML 文件读入 XElement 对象,执行所需的操作,然后保存XElement回到文件,然后释放锁。
  4. 任何时候,当用户正在修改文件时(某些操作可能会很长),任何其他试图进行修改的用户不得能够打开文件文件(我将捕获异常,并显示 "Please wait a few moments" 类型的消息)。但是,他们必须能够以只读方式打开文件以执行查询。

我已经看到 ,并尝试使用以下代码实现它。在 PowerShell ISE 的一个实例中,我执行了读写函数,然后在 "Enter an item name" 循环中,我在另一个 PowerShell ISE 实例中执行了只读函数。尝试以只读方式打开文档时,我收到异常 The process cannot access the file 'C:\Path\To\file.xml' because it is being used by another process.

using namespace System.Linq.Xml
using namespace System.IO
Add-Type -AssemblyName 'System.Xml.Linq (rest of Assembly's Full Name)'

$filename = "C:\Path\To\file.xml"

function Read-Only()
{
    $filestream = [FileStream]::new($filename, [FileMode]::Open, [FileAccess]::Read)
    $database = [XElement]::Load($filestream)
    foreach($item in $database.Element("Items").Elements("Item"))
    {
        Write-Host "Item name $($item.Attribute("Name").Value)"
    }
    $filestream.Close()
    $filestream.Dispose()
}
function Read-Write()
{
    $filestream = [FileStream]::new($filename, [FileMode]::Open, [FileAccess]::ReadWrite, [FileShare]::Read)
    $database = [XElement]::Load($filestream)

    $itemname = Read-Host "Enter an item name ('quit' to quit)"
    while($itemname -ne 'quit')
    {
        $database.Element("Items").Add([XElement]::new([XName]"Item", [XAttribute]::new([XName]"Name", $itemname)))
        $itemname = Read-Host "Enter an item name ('quit' to quit)"
    }

    $filestream.Seek(0, [SeekOrigin]::Begin)
    $database.Save($filestream)

    $filestream.Close()
    $filestream.Dispose()
}

如何锁定文件进行独占编辑,同时仍允许对任何其他客户端进行只读访问?

编辑: 我已经使用 mklement0 建议的解决方案解决了这个问题 - 使用单独的文件作为锁定文件。代码张贴在这里:https://pastebin.com/ytzGE7se

我可以建议一个稍微不同的方法来解决这一切。您可以使用 PowerShell 执行以下操作,从而避免读取问题。我的 XML 导航可能有点不对劲,但它会像描述的那样工作 here

function Read-Only()
{
    $database = [xml](Get-Content $filename)
    foreach($item in $database.Items)
    {
        Write-Host "Item name $item.Name"
    }
 }

现在您可以保留相同的写入功能,也可以使用 PowerShell 完成所有操作。如果你想用 PowerShell 来做,你可以这样做。

function Read-Write()
{
    $database = [xml](Get-Content $filename)

    $itemname = Read-Host "Enter an item name ('quit' to quit)"
    while($itemname -ne 'quit')
    {
        $newItem = $database.CreateElement("item")
        $newItem.SetAttribute("Name", $itemname)
        $database.Items.AppendChild($newItem)
        # You can also save all the changes up and write once.
        $database.Save($filename)
        $itemname = Read-Host "Enter an item name ('quit' to quit)"
    }
}

也许这会有所帮助。创建文件流时,有一个 FileShare 选项。如果将其设置为读写,则多个进程可以打开该文件

$fsMain = [System.IO.File]::Open("C:\stack\out.txt", "Open", "ReadWrite", "ReadWrite")
$fsReadOnly = [System.IO.File]::Open("C:\stack\out.txt", "Open", "Read", "ReadWrite")

Write-Host ("fsMain:  CanRead=" + $fsMain.CanRead + ", CanWrite=" + $fsMain.CanWrite)
Write-Host ("fsReadOnly:  CanRead= " + $fsReadOnly.CanRead + ", CanWrite=" + $fsReadOnly.CanWrite)

$fsMain.Close()
$fsReadOnly.Close()

如果您使用[FileAccess]::Read而没有明确指定文件共享模式,那么如果文件已经打开,只有当它最初是使用文件共享模式打开时,再次打开它才会成功[FileShare]::ReadWrite(即使您只请求 read 访问权限,方法 defaults 请求 write 访问权限也是) - 而你的 Read-Write 函数(明智地)只使用 [FileShare]::Read.

如果您使用文件共享模式显式打开只读文件流,您眼前的问题就会消失[FileShare]::ReadWrite:

[System.IO.FileStream]::new(
  $path, 
  [System.IO.FileMode]::Open, 
  [System.IO.FileAccess]::Read, 
  [System.IO.FileShare]::ReadWrite  # !! required
)

这允许其他读者并发访问以及唯一的 (read+)writer(可能首先打开文件)。

但是,一个文件在被其他人读取时被重写可能会有问题,因此为了稳健和可预测操作 我建议不同的方法:

注意:相关问题的显示了一个更简单的替代下面的解决方案,但是,更新文件需要更长的时间


在文件临时副本中进行修改,然后替换原始文件。

需要显式同步来协调潜在的 更新者 以便 序列化 更新 以防止更新相互覆盖。
您可以使用 一个单独的锁定文件 (哨兵文件)来实现这一点,例如,命名为 updating,它可以向其他潜在的作者指示更新正在进行中.

Mike Christiansen(OP 本人)最终还将锁定用户的用户名存储在该文件中,以向其他可能的储物柜提供反馈。

当请求修改时:

  • 循环尝试直到创建锁定文件updating成功,如果文件已经存在则失败。

    • (重新)-创建 updating 文件向其他可能的作者发出修改已经开始的信号。相比之下,读者此时可以继续阅读。
  • 创建当前 XML 文件的(临时)副本 并在其中执行修改。

    • Mike 自己最终只是简单地修改了文件的 内存中 副本,这简化了事情(如果您有足够的内存来读取整个文件)。
  • 用修改后的副本替换原始文件 - 使用重试循环直到复制/重写原始文件成功,因为其他读者可能会暂时阻止删除(重新创建)/重写原始文件.

  • 删除文件 updating,向其他可能的修改者发出更新已完成的信号。

    • 确保文件总是清理(try .. finally),因为让它逗留会阻止未来的更新;您可能还需要一个基于超时的机制,如果可以假定先前的更新程序已崩溃,则在等待最终删除预先存在的文件时 强制 删除。

至于阅读访问:

  • 虽然没有进行任何修改,但并发读取访问应该可以正常工作。

  • 当 XML 文件被临时副本替换/重写时,在文件复制操作期间打开文件进行读取将失败 / 写操作,所以你也需要一个重试循环。


可以找到 Mike 最终使用的代码 here