在C#中处理文件并发访问时,我们常常不需要把整个文件锁死,而是只想保护其中的一小段数据。比如多个进程同时向同一个大文件的不同区域写入,如果粗暴地使用独占打开,会严重拖慢系统吞吐。FileStream类自带的两个方法Lock和Unlock,正好能让我们针对文件的某一段字节区间加锁,实现更细粒度的控制。

一、FileStream的Lock与Unlock方法原理
FileStream的Lock(long position, long length)方法会对当前文件流所关联的底层文件句柄,施加一个区域锁。这个锁是咨询锁(advisory lock),由操作系统文件系统层面记录,其他进程若试图读取或写入被锁定的区间,会收到访问拒绝的错误。position表示锁定起始字节偏移量,length表示向后锁定的字节数,两者都必须是非负整数,且position加length不能超过文件长度(或者在锁定前先扩展文件)。
Unlock(long position, long length)用于释放之前锁定的区域,参数必须与加锁时完全一致,否则会抛出ArgumentException。需要特别注意的是,锁是与进程和文件句柄绑定的,如果程序退出或流被关闭,系统会自动释放该句柄上的所有锁,不必手动Unlock。但在长时间运行的服务中,明确解锁能减少资源占用,也避免后续逻辑误以为区域仍被占用。
1.1 区域锁的重叠限制
在同一文件句柄上,已经锁定的区域不能再被同一进程重复锁定不同区间,但Windows允许在已解锁后重新锁定。更重要的是,Lock锁定的是物理字节区间,而不是逻辑记录,因此调用前必须准确计算你要保护的二进制范围。例如一个固定长度记录文件,每条记录512字节,要锁第10条记录就应传入position=512*9,length=512。
如果多个进程都使用Lock,后一个进程锁定与前一个进程重叠的区间时,会立即得到IOException,提示文件区域已被锁定。这种机制非常适合用来做简单的多进程写入调度,而不必引入额外的互斥量文件。
二、基础代码示例
下面展示一个完整的控制台程序,它打开(或创建)一个文件,向第0条记录写入数据,并在写入期间锁定该记录区间,写完再解锁。我们故意用using管理FileStream,确保异常时也能释放句柄。
using System;
using System.IO;
using System.Text;
class Program
{
static void Main()
{
string path = "test.bin";
// 如果文件不存在则创建,并预分配1024字节
using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite))
{
if (fs.Length < 1024)
{
fs.SetLength(1024);
}
long recordSize = 512;
long position = 0; // 锁定第一条记录
long length = recordSize;
try
{
fs.Lock(position, length);
Console.WriteLine("已锁定字节区间:" + position + " 到 " + (position + length));
fs.Seek(position, SeekOrigin.Begin);
byte[] data = Encoding.UTF8.GetBytes("hello locked region".PadRight((int)recordSize));
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (IOException ex)
{
Console.WriteLine("锁定或写入失败:" + ex.Message);
}
finally
{
fs.Unlock(position, length);
Console.WriteLine("已解锁该区间");
}
}
}
}
上述代码中,FileShare.ReadWrite允许其他进程同时以读写方式打开文件,但我们的Lock依然能阻止它们碰被锁的那512字节。如果另一个进程尝试在锁生效时调用Write覆盖同区间,就会抛出IOException,提示部分文件不可访问。
在实际项目中,可以把记录大小和索引抽象成方法参数,配合重试逻辑:若Lock抛异常,说明别的进程正占用,可短暂等待后重试,从而实现轻量级的多进程队列写入。
三、常见误区与异常处理
不少开发者误以为Lock能让当前进程内多线程自动互斥,其实它只防其他进程,同一进程内多个线程操作同一FileStream本身就需要自己用lock语句同步。另外,有人把position传成负数或length超过文件末尾却未提前SetLength,结果得到IOException而非自动扩展,这是系统底层限制,必须提前规划文件尺寸。
还有一点容易忽略:Lock和Unlock必须在同一个FileStream实例上成对出现,且参数值必须逐字节相同。如果把流关闭了再Unlock,会抛ObjectDisposedException。推荐写法是用try-finally,或在using块内部完成加锁与解锁,确保哪怕中间代码抛异常,finally里的Unlock也能执行(若流已关则跳过)。
3.1 异常类型对照
| 异常类型 | 触发原因 | 处理建议 |
|---|---|---|
| IOException | 区域已被其他进程锁定,或参数越界 | 检查区间计算,加入重试 |
| ArgumentException | Unlock参数与Lock不一致 | 保存加锁时的position和length |
| ObjectDisposedException | 流已关闭仍尝试解锁 | 在using内完成解锁 |
通过上表可以快速定位大部分文件区域锁相关的问题。建议在封装工具类时,把position和length存为私有字段,公开方法内部自动配对,降低人为出错概率。
四、与整体锁的对比及适用场景
若使用FileShare.None打开文件,等于让整个文件在打开期间排斥其他所有访问,简单但粗暴。Lock方案的优势在于并发度高:进程A锁头部,进程B可同时锁尾部,彼此不干扰。适合日志分段、多生产者单文件分区存储等场景。
不过Lock依赖操作系统支持,在部分网络映射盘或FAT格式卷上行为可能不一致,重要业务建议先在目标环境做兼容性测试。当需要确保跨平台一致时,可考虑用单独锁文件配合FileStream.Lock做双保险,既利用系统锁又保留应用层标记。
五、小结
利用FileStream的Lock和Unlock,C#开发者能够精确锁定文件中的某一段字节,而不是整份文件。正确计算偏移与长度、处理好异常、理解其跨进程而非跨线程的特性,就能在并发文件操作中既保安全又保性能。把区域锁逻辑封装清楚,你的多进程文件写入模块会更加健壮。
C#FileStreamLock修改时间:2026-08-06 23:09:33