C# 中的并发文件使用
Concurrent file usage in C#
我有一个应用程序将从文件夹中读取并等待文件出现在该文件夹中。当这个文件出现时,应用程序应读取内容,使用文件中的数据对外部系统执行一些功能,然后删除文件(然后等待下一个文件)。
现在,我想在两台不同的机器上 运行 这个应用程序,但都在同一个文件夹中侦听。所以它是完全相同的应用程序,但有两个实例。我们称它为实例 A 和实例 B。
所以当一个新文件出现时,A和B都会找到这个文件,并且都会尝试读取它。这将导致两个实例之间出现某种竞争条件。我希望如果 A 在 B 之前开始读取文件,B 将简单地跳过该文件并让 A 处理并删除它。同样,如果 B 先找到文件,A 什么都不做。
现在我该如何实现这个,在文件上设置锁是不够的我猜是因为假设 A 开始读取文件,然后它被 A 锁定,然后 A 将解锁它以删除它.在此期间,B 可能会尝试读取该文件。在那种情况下,文件会被处理两次,这是不可接受的。
总而言之,只要文件夹中出现一个文件,我就有一个程序和一个文件夹/网络共享的两个实例。我希望实例 A 或实例 B 处理该文件。从来没有,关于如何在 C# 中实现此类功能的任何想法?
我可以想到两个快速解决方案;
分配负载
让您的 2 个进程只对某些文件起作用。您如何执行此操作可能基于文件名或 date/time。例如。进程 1 读取时间戳以奇数结尾的文件,进程 2 读取以偶数结尾的文件。
数据库作为锁
另一种选择是使用某种数据库作为锁。
进程 1 读取文件并根据文件名(必须是唯一的)插入数据库 table。如果插入有效,那么它负责文件并继续处理它,否则如果插入失败,那么另一个进程已经插入它所以它负责并且进程 1 忽略该文件。
两个进程都必须可以访问数据库,这会产生一些开销。但如果您想将其扩展到更多进程,可能是更好的选择。
正确的做法是用写锁(例如System.IO.FileAccess.Write和读共享(例如System.IO.FileShare.Read)打开文件。如果其中一个进程试图当其他进程已经打开文件时打开文件,然后打开命令将抛出异常,您需要根据需要捕获和处理异常(例如,记录并重试)。通过对打开的文件使用写锁,您保证打开和锁定是原子的,因此在两个进程之间是同步的,并且没有竞争条件。
所以像这样:
try
{
using (FileStream fileStream = new FileStream(FileName, FileMode.Open, FileAccess.Write, FileShare.Read))
{
// Read from or write to file.
}
}
catch (IOException ex)
{
// The file is locked by the other process.
// Some options here:
// Log exception.
// Ignore exception and carry on.
// Implement a retry mechanism to try opening the file again.
}
如果您不希望其他进程在您的程序打开文件时访问该文件,则可以使用 FileShare.None。我更喜欢 FileShare.Read 因为它允许我监视文件中发生的事情(例如,在记事本中打开它)。
为了迎合删除文件也是类似的原理:先rename/move文件,捕获如果其他进程已经重命名it/moved它时发生的IOException,然后打开renamed/moved 文件。您 rename/move 文件表明该文件已经在处理中,应该被其他进程忽略。例如,将其重命名为 .pending 文件扩展名,或将其移至 Pending 目录。
try
{
// This will throw an exception if the other process has already moved the file -
// either FileName no longer exists, or it is locked.
File.Move(FileName, PendingFileName);
// If we get this far we know we have exclusive access to the pending file.
using (FileStream fileStream = new FileStream(PendingFileName, FileMode.Open, FileAccess.Write, FileShare.Read))
{
// Read from or write to file.
}
File.Delete(PendingFileName);
}
catch (IOException ex)
{
// The file is locked by the other process.
// Some options here:
// Log exception.
// Ignore exception and carry on.
// Implement a retry mechanism to try moving the file again.
}
与打开文件一样,File.Move 是原子的并受锁保护,因此可以保证如果您有多个并发 threads/processes 尝试移动文件,只有一个会成功,其他的会成功会抛出异常。请参阅此处了解类似问题:Atomicity of File.Move.
我建议使用 functionality-server 方法,而不是深入研究文件访问更改。这种方法的另一个论点是来自不同计算机的文件使用。这个特别的事情深入到访问和权限管理中。
我的建议是实现以下功能的单点文件访问(文件存储库):
- 获取文件列表。 (获取可用文件列表)
- 签出文件。 (专有的获取文件的权限,以便结账的所有者被授权修改文件)
- 修改文件。 (更新文件内容或删除它)
- 将更改签入存储库
有很多方法可以实现该方法。 (使用文件的 API 文件版本控制系统;实施服务;使用数据库,...)
简单(需要支持事务、触发器或存储过程的数据库)
- 获取文件列表。 (SQL SELECT 来自 "available files table")
- 签出文件。 (SQL UPDATE 或更新存储过程。通过在触发器或存储过程中更新定义一个 "raise error" 状态以防多次检出)
- 修改文件。 (更新文件内容或将其删除。请记住,最好对功能做更多操作"server"。在这种情况下,您需要实施一次安全策略)
- 对存储库的签入更改(释放特定文件条目的 "Checked Out" 字段。在事务中实现签入)
所以如果你要申请锁你可以尝试使用文件名作为锁对象。您可以尝试以特殊方式重命名文件(例如在文件名前添加点)
幸运地重命名文件的第一个服务将继续使用它。第二个(慢)会得到文件不存在的异常。
并且您必须向文件处理逻辑添加检查,以确保服务不会尝试 "lock" 已经 "locked" 的文件(名称以点开头)。
UPD 可能最好包括特殊字符集(如标记)和一些服务标识符(机器名与 PID 连接)
因为我不确定文件重命名将如何在并发模式下工作。
所以如果你在共享文件夹
中有file.txt
- 首先你必须检查文件名中是否有.lock字符串
已经
- 如果没有服务可以尝试将其重命名为 file.txt.lockDevhost345(其中
.lock
- 特殊标记,Devhost
- 当前计算机的名称,345
是PID(进程标识符)
- 然后服务必须检查是否有
file.txt.lockDevhost345
文件
可用
如果是 - 它被当前服务实例锁定并且可以使用
如果不是 - 它是由并发服务 "stolen" 处理的,因此不应处理。
如果您没有写入权限,您可以使用另一个网络共享并尝试创建额外的文件锁定标记,例如对于 file.txt
服务可以尝试创建(并保持写入锁定)新文件,例如 file.txt.lock
第一个创建锁定文件的服务正在处理原始文件,只有在处理原始文件时才删除锁定。
我有一个应用程序将从文件夹中读取并等待文件出现在该文件夹中。当这个文件出现时,应用程序应读取内容,使用文件中的数据对外部系统执行一些功能,然后删除文件(然后等待下一个文件)。
现在,我想在两台不同的机器上 运行 这个应用程序,但都在同一个文件夹中侦听。所以它是完全相同的应用程序,但有两个实例。我们称它为实例 A 和实例 B。
所以当一个新文件出现时,A和B都会找到这个文件,并且都会尝试读取它。这将导致两个实例之间出现某种竞争条件。我希望如果 A 在 B 之前开始读取文件,B 将简单地跳过该文件并让 A 处理并删除它。同样,如果 B 先找到文件,A 什么都不做。
现在我该如何实现这个,在文件上设置锁是不够的我猜是因为假设 A 开始读取文件,然后它被 A 锁定,然后 A 将解锁它以删除它.在此期间,B 可能会尝试读取该文件。在那种情况下,文件会被处理两次,这是不可接受的。
总而言之,只要文件夹中出现一个文件,我就有一个程序和一个文件夹/网络共享的两个实例。我希望实例 A 或实例 B 处理该文件。从来没有,关于如何在 C# 中实现此类功能的任何想法?
我可以想到两个快速解决方案;
分配负载
让您的 2 个进程只对某些文件起作用。您如何执行此操作可能基于文件名或 date/time。例如。进程 1 读取时间戳以奇数结尾的文件,进程 2 读取以偶数结尾的文件。
数据库作为锁
另一种选择是使用某种数据库作为锁。
进程 1 读取文件并根据文件名(必须是唯一的)插入数据库 table。如果插入有效,那么它负责文件并继续处理它,否则如果插入失败,那么另一个进程已经插入它所以它负责并且进程 1 忽略该文件。
两个进程都必须可以访问数据库,这会产生一些开销。但如果您想将其扩展到更多进程,可能是更好的选择。
正确的做法是用写锁(例如System.IO.FileAccess.Write和读共享(例如System.IO.FileShare.Read)打开文件。如果其中一个进程试图当其他进程已经打开文件时打开文件,然后打开命令将抛出异常,您需要根据需要捕获和处理异常(例如,记录并重试)。通过对打开的文件使用写锁,您保证打开和锁定是原子的,因此在两个进程之间是同步的,并且没有竞争条件。
所以像这样:
try
{
using (FileStream fileStream = new FileStream(FileName, FileMode.Open, FileAccess.Write, FileShare.Read))
{
// Read from or write to file.
}
}
catch (IOException ex)
{
// The file is locked by the other process.
// Some options here:
// Log exception.
// Ignore exception and carry on.
// Implement a retry mechanism to try opening the file again.
}
如果您不希望其他进程在您的程序打开文件时访问该文件,则可以使用 FileShare.None。我更喜欢 FileShare.Read 因为它允许我监视文件中发生的事情(例如,在记事本中打开它)。
为了迎合删除文件也是类似的原理:先rename/move文件,捕获如果其他进程已经重命名it/moved它时发生的IOException,然后打开renamed/moved 文件。您 rename/move 文件表明该文件已经在处理中,应该被其他进程忽略。例如,将其重命名为 .pending 文件扩展名,或将其移至 Pending 目录。
try
{
// This will throw an exception if the other process has already moved the file -
// either FileName no longer exists, or it is locked.
File.Move(FileName, PendingFileName);
// If we get this far we know we have exclusive access to the pending file.
using (FileStream fileStream = new FileStream(PendingFileName, FileMode.Open, FileAccess.Write, FileShare.Read))
{
// Read from or write to file.
}
File.Delete(PendingFileName);
}
catch (IOException ex)
{
// The file is locked by the other process.
// Some options here:
// Log exception.
// Ignore exception and carry on.
// Implement a retry mechanism to try moving the file again.
}
与打开文件一样,File.Move 是原子的并受锁保护,因此可以保证如果您有多个并发 threads/processes 尝试移动文件,只有一个会成功,其他的会成功会抛出异常。请参阅此处了解类似问题:Atomicity of File.Move.
我建议使用 functionality-server 方法,而不是深入研究文件访问更改。这种方法的另一个论点是来自不同计算机的文件使用。这个特别的事情深入到访问和权限管理中。
我的建议是实现以下功能的单点文件访问(文件存储库):
- 获取文件列表。 (获取可用文件列表)
- 签出文件。 (专有的获取文件的权限,以便结账的所有者被授权修改文件)
- 修改文件。 (更新文件内容或删除它)
- 将更改签入存储库
有很多方法可以实现该方法。 (使用文件的 API 文件版本控制系统;实施服务;使用数据库,...)
简单(需要支持事务、触发器或存储过程的数据库)
- 获取文件列表。 (SQL SELECT 来自 "available files table")
- 签出文件。 (SQL UPDATE 或更新存储过程。通过在触发器或存储过程中更新定义一个 "raise error" 状态以防多次检出)
- 修改文件。 (更新文件内容或将其删除。请记住,最好对功能做更多操作"server"。在这种情况下,您需要实施一次安全策略)
- 对存储库的签入更改(释放特定文件条目的 "Checked Out" 字段。在事务中实现签入)
所以如果你要申请锁你可以尝试使用文件名作为锁对象。您可以尝试以特殊方式重命名文件(例如在文件名前添加点) 幸运地重命名文件的第一个服务将继续使用它。第二个(慢)会得到文件不存在的异常。
并且您必须向文件处理逻辑添加检查,以确保服务不会尝试 "lock" 已经 "locked" 的文件(名称以点开头)。
UPD 可能最好包括特殊字符集(如标记)和一些服务标识符(机器名与 PID 连接) 因为我不确定文件重命名将如何在并发模式下工作。 所以如果你在共享文件夹
中有file.txt
- 首先你必须检查文件名中是否有.lock字符串 已经
- 如果没有服务可以尝试将其重命名为 file.txt.lockDevhost345(其中
.lock
- 特殊标记,Devhost
- 当前计算机的名称,345
是PID(进程标识符) - 然后服务必须检查是否有
file.txt.lockDevhost345
文件 可用
如果是 - 它被当前服务实例锁定并且可以使用 如果不是 - 它是由并发服务 "stolen" 处理的,因此不应处理。
如果您没有写入权限,您可以使用另一个网络共享并尝试创建额外的文件锁定标记,例如对于 file.txt
服务可以尝试创建(并保持写入锁定)新文件,例如 file.txt.lock
第一个创建锁定文件的服务正在处理原始文件,只有在处理原始文件时才删除锁定。