我有一个需要并发访问的 XML 文件。我不需要并发写入,但我确实需要并发读取。读取/修改此 XML 文件的规定方法是使用 PowerShell 脚本,该脚本使用 Linq to XML (XElement) 来处理 XML 文件。
理想的场景是:
- 团队中的所有用户都将在工作日开始时运行此 PowerShell 脚本,并将一直运行到工作日结束。
- 在整个工作日,团队中的任何用户都将输入预定义的命令来查询 XML 文件以收集数据。每次用户运行这些查询之一时,它都会打开 XML 文件,执行查询,关闭 XML 文件,然后显示结果。
- 有时,在一天中,这些用户中的任何一个都需要修改 XML 文件。用户将输入一个预定义的命令,指定他们想要修改的节点,然后他们将输入他们想要添加/修改的所有数据。然后脚本将打开文件(将其锁定,因此其他用户无法以读/写方式打开它),将 XML 文件读入
XElement
对象,执行所需的操作,然后保存XElement
返回文件,然后释放锁。 - 在任何时候,当用户正在修改文件时(某些操作可能会很长),任何其他试图进行修改的用户不得能够打开该文件(我会捕获异常,并显示“请稍等片刻”类型的消息)。但是,他们必须能够以只读方式打开文件以执行查询。
我看过this related post, 'Concurrent file usage in C#' ,并尝试使用以下代码实现这一点。在 PowerShell ISE 的一个实例中,我执行了读写函数,然后在“输入项目名称”循环中,我在另一个 PowerShell ISE 实例中执行了只读函数。尝试以只读方式打开文档时,我收到一个异常,指出 进程无法访问文件“C:\Path\To\file.xml”,因为它正被另一个进程使用。
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
最佳答案
如果您使用 [FileAccess]::Read
而不明确指定文件共享模式,那么如果文件已经打开,只有在最初使用文件打开时再次打开它才会成功-共享模式 [FileShare]::ReadWrite
(即使您只请求读取访问权限,该方法默认请求写入 也可以访问)- 而您的 Read-Write
函数(明智地)仅使用 [FileShare]::Read
。
如果您使用文件共享模式[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
文件向其他可能的作者发出修改已经开始的信号。相比之下,读者此时可以继续阅读。
- (重新)创建
创建当前 XML 文件的(临时)副本并在其中执行修改。
- Mike 自己最终只是修改了文件的内存副本,这简化了事情(如果您有足够的内存来读取整个文件)。
用修改后的副本替换原始文件 - 使用重试循环直到成功复制/重写原始文件,因为其他读者可能会暂时阻止删除(重新创建)/重写原始文件。
删除文件
正在更新
,向其他可能的修改者发出更新已完成的信号。- 确保文件总是被清理(
try .. finally
),因为让它逗留会阻止 future 的更新;您可能还需要一种基于超时的机制,如果可以假定先前的更新程序已崩溃,则在等待删除预先存在的文件时最终强制删除。
- 确保文件总是被清理(
至于读取权限:
虽然没有进行任何修改,但并发读取访问应该可以正常工作。
当 XML 文件被临时副本替换/重写时,打开文件进行读取将在文件复制操作期间/写入操作失败,因此您那里也需要一个重试循环。
Mike 最终使用的代码可以找到here .
关于c# - .NET (PowerShell) 并发文件使用(锁定,同时仍允许读取访问),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52837664/