Unity C# 连续读写文件触发Sharing violation共享违例问题排查
问题根因
抛出共享违例异常是两个核心问题叠加导致,和你是否串行执行await逻辑没有直接关联:
- 文件名处理逻辑不一致:
LoadSession方法直接将传入的fileName拼接到持久化目录路径下,没有追加后缀;SaveSession方法会硬编码给传入的文件名拼上.sav后缀。如果两边传参没有严格对齐,要么读写的是完全不同的文件,要么会出现路径匹配偏差导致的锁冲突。 - Windows内核文件锁释放延迟:你预期
using块结束后文件句柄立刻释放、文件锁立刻移除,这个认知不符合NTFS的实际运行逻辑。用户态调用Stream.Dispose()时,只会触发Win32 APICloseHandle标记句柄待回收,内核层面的文件锁移除、对象回收是异步调度的,延迟通常在几毫秒到几十毫秒不等。你在加载完成后立刻以FileShare.None独占模式打开文件请求写权限,刚好撞上这个锁释放的空窗期就会抛出异常。 - 额外隐患:你手动分块读取字节再转
Encoding.Unicode的逻辑存在字符截断风险,Unicode是双字节编码,如果单次读取刚好截断一个双字节字符,反序列化时会出现乱码。
修复方案
按以下顺序调整代码即可彻底解决问题:
- 抽离统一的路径处理逻辑,避免读写两边各自拼接路径导致的不一致:
private string GetSaveFilePath(string fileName) { // 统一处理后缀,避免重复加.sav或漏加 if (!fileName.EndsWith(".sav")) fileName += ".sav"; return Path.Combine(Application.persistentDataPath, fileName); }
- 实现带重试的文件打开逻辑,处理内核锁释放延迟的场景,这是工业级文件IO代码的标准实践:
/// <summary> /// 打开文件流,遇到共享违例时自动重试 /// </summary> /// <param name="maxRetries">最大重试次数</param> /// <param name="retryDelayMs">单次重试间隔(毫秒)</param> private async Task<FileStream> OpenFileStreamWithRetry(string path, FileMode mode, FileAccess access, FileShare share, int maxRetries = 5, int retryDelayMs = 10) { int retryCount = 0; while (true) { try { return new FileStream(path, mode, access, share, bufferSize: 4096, useAsync: true); } catch (IOException ex) when (retryCount < maxRetries && (ex.HResult & 0xFFFF) == 32) { // 系统错误码32对应ERROR_SHARING_VIOLATION,即共享违例 retryCount++; await Task.Delay(retryDelayMs); } } }
- 重构读写逻辑,替换原有的直接new FileStream的实现,同时修复编码截断问题:
- 加载逻辑重构:
public async Task<GameState> LoadSession(string fileName) { var path = GetSaveFilePath(fileName); if (!File.Exists(path)) return null; using var sourceStream = await OpenFileStreamWithRetry(path, FileMode.Open, FileAccess.Read, FileShare.Read); using var reader = new StreamReader(sourceStream, Encoding.Unicode); string content = await reader.ReadToEndAsync(); return JsonConvert.DeserializeObject<GameState>(content); }
- 保存逻辑调整:将
FileShare.None改为FileShare.Read,允许其他逻辑在写入时以读权限共享访问,进一步降低锁冲突概率:
public async Task SaveSession(string fileName) { var path = GetSaveFilePath(fileName); byte[] encodedText = Encoding.Unicode.GetBytes(serializedObject); using var outputFile = await OpenFileStreamWithRetry(path, FileMode.Create, FileAccess.Write, FileShare.Read); await outputFile.WriteAsync(encodedText, 0, encodedText.Length); }
- 排查事件订阅逻辑:确认
OnSessionLoaded事件的其他订阅者没有同时持有目标存档文件的句柄,如果有其他读逻辑也要及时释放流。
底层逻辑说明
.NET的using语句只能保证当前托管的FileStream对象被正确Dispose,触发非托管句柄的关闭请求,但无法控制Windows内核的调度节奏。NTFS的文件锁是内核级对象,不存在用户态可以同步等待锁完全释放的接口,因此短暂的锁占用空窗期是正常现象,必须通过重试机制兼容。FileShare枚举参数仅在句柄持有期间生效,定义的是当前句柄存在时,其他打开请求需要满足的权限规则,句柄关闭后这些规则不会残留,不需要担心锁泄漏问题。
内容的提问来源于stack exchange,提问作者wau31
相关产品推荐
相关产品推荐

