You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity C# 连续读写文件触发Sharing violation共享违例问题排查

问题根因

抛出共享违例异常是两个核心问题叠加导致,和你是否串行执行await逻辑没有直接关联:

  • 文件名处理逻辑不一致:LoadSession方法直接将传入的fileName拼接到持久化目录路径下,没有追加后缀;SaveSession方法会硬编码给传入的文件名拼上.sav后缀。如果两边传参没有严格对齐,要么读写的是完全不同的文件,要么会出现路径匹配偏差导致的锁冲突。
  • Windows内核文件锁释放延迟:你预期using块结束后文件句柄立刻释放、文件锁立刻移除,这个认知不符合NTFS的实际运行逻辑。用户态调用Stream.Dispose()时,只会触发Win32 API CloseHandle标记句柄待回收,内核层面的文件锁移除、对象回收是异步调度的,延迟通常在几毫秒到几十毫秒不等。你在加载完成后立刻以FileShare.None独占模式打开文件请求写权限,刚好撞上这个锁释放的空窗期就会抛出异常。
  • 额外隐患:你手动分块读取字节再转Encoding.Unicode的逻辑存在字符截断风险,Unicode是双字节编码,如果单次读取刚好截断一个双字节字符,反序列化时会出现乱码。
修复方案

按以下顺序调整代码即可彻底解决问题:

  1. 抽离统一的路径处理逻辑,避免读写两边各自拼接路径导致的不一致:
private string GetSaveFilePath(string fileName)
{
    // 统一处理后缀,避免重复加.sav或漏加
    if (!fileName.EndsWith(".sav"))
        fileName += ".sav";
    return Path.Combine(Application.persistentDataPath, fileName);
}
  1. 实现带重试的文件打开逻辑,处理内核锁释放延迟的场景,这是工业级文件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);
        }
    }
}
  1. 重构读写逻辑,替换原有的直接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);
}
  1. 排查事件订阅逻辑:确认OnSessionLoaded事件的其他订阅者没有同时持有目标存档文件的句柄,如果有其他读逻辑也要及时释放流。
底层逻辑说明

.NET的using语句只能保证当前托管的FileStream对象被正确Dispose,触发非托管句柄的关闭请求,但无法控制Windows内核的调度节奏。NTFS的文件锁是内核级对象,不存在用户态可以同步等待锁完全释放的接口,因此短暂的锁占用空窗期是正常现象,必须通过重试机制兼容。
FileShare枚举参数仅在句柄持有期间生效,定义的是当前句柄存在时,其他打开请求需要满足的权限规则,句柄关闭后这些规则不会残留,不需要担心锁泄漏问题。


内容的提问来源于stack exchange,提问作者wau31

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 21:15:44