【问题标题】:.NET (PowerShell) Concurrent file usage (locking, while still allowing read access).NET (PowerShell) 并发文件使用(锁定,同时仍允许读取访问)
【发布时间】:2019-03-21 02:48:14
【问题描述】:

我有一个需要同时访问的 XML 文件。我不需要并发写入,但我确实需要并发读取。读取/修改此 XML 文件的规定方法是使用 PowerShell 脚本,该脚本使用 Linq to XML (XElement) 来处理 XML 文件。

理想的场景是:

  1. 团队中的所有用户都将在工作日开始时运行此 PowerShell 脚本,并将一直运行到工作日结束。
  2. 在整个工作日期间,团队中的任何用户都将输入预定义的命令,这些命令将查询 XML 文件以收集数据。每次用户运行其中一个查询时,它都会打开 XML 文件,执行查询,关闭 XML 文件,然后显示结果。
  3. 有时,这些用户中的任何一个都需要修改 XML 文件。用户将输入一个预定义的命令,指定他们想要修改的节点,然后他们将输入他们想要添加/修改的所有数据。然后脚本将打开文件(锁定它,因此其他用户无法以读/写方式打开它),将 XML 文件读入XElement 对象,执行所需的操作,然后将XElement 保存回文件,然后释放锁。
  4. 在任何时候,当用户修改文件时(某些操作可能很长),任何其他尝试进行修改的用户不得能够打开文件(我会发现异常,并显示“请稍等”类型的消息)。但是,他们必须能够以只读方式打开文件才能执行查询。

我见过this related post, 'Concurrent file usage in C#',并尝试使用以下代码来实现这一点。在 PowerShell ISE 的一个实例中,我执行读写函数,然后在“输入项目名称”循环中,我在另一个 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

【问题讨论】:

  • 为什么不在编辑前修改 ACL 并在最后重置?
  • 这似乎有点笨拙,尤其是看到有可能在不修改 ACL 的情况下做到这一点?

标签: c# .net xml powershell linq-to-xml


【解决方案1】:

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

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

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

这允许其他读取器同时访问以及唯一的(读取+)写入器(可能首先打开文件)。

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

注意:This answer 对相关问题显示了以下解决方案的更简单的替代方法,但是,更新需要更长的时间文件。


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

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

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

修改请求时:

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

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

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

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

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

至于读取权限:

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

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


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

【讨论】:

  • 感谢@MikeChristiansen 的反馈。将整个文件加载到内存中(如果可行的话)肯定会简化事情。将用户名添加到锁定文件是一个好主意。我已经相应地更新了答案,并且还包含了指向您的代码的链接。
  • 是的,此时XML文件只有1MB。也许我以后必须从磁盘而不是内存中读取,但现在,这很好。
【解决方案2】:

我可以建议一种稍微不同的方法。您可以使用 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)"
    }
}

【讨论】:

  • 但我仍然有这种竞争条件。人员 A 读取 XML 文件。人员 B 读取 XML 文件。人员 A 更改项目并保存。人 B 改变了一个不同的项目,并保存。人员 A 的更改丢失 - 因为人员 B 没有收到人员 A 的更改。
  • 我会采用乐观锁定方法。如果时间戳在用户打开写入后发生了变化,那么我会更新内容然后提交他们的更改。
  • 但我仍然有这种竞争条件。如果时间戳在我检查和更新之间发生变化怎么办?
  • 我会用git来解决。因为团队成员甚至可以离线进行更改,之后仍然可以推送更改而不会覆盖其他人的工作。
  • @MatheusValienteSouza - 不幸的是,git 不是一个选项。
【解决方案3】:

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

$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()

【讨论】:

  • 在我的原始帖子中,我使用了 FileShare 枚举 - 它不会产生预期的效果。
  • FileShare 应该同时出现在 Read-only 和 Read-Write 函数上,并且应该在这两个函数上都设置为 ReadWrite(就像在我的示例中一样)。
  • 现在,不会发生错误,但两个单独的用户能够同时以读写方式打开文件。人员 A 加载了文件,人员 B 加载了文件,人员 A 进行了更改,人员 B 进行了更改,人员 A 保存了更改,人员 B 保存了更改。但是,由于操作顺序,A 的更改被丢弃了。它没有阻止人 B 按需要以读写模式打开文件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-02
  • 2022-01-16
  • 2011-03-17
  • 1970-01-01
  • 2021-06-17
  • 2013-12-30
  • 1970-01-01
相关资源
最近更新 更多