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

多线程调用CopyToAsync异步写文件偶现乱码问题排查

问题根因

你的多线程写文件逻辑存在多处确定性错误,乱码不是随机故障,是竞态条件导致的必然结果:

  • 基础语法/设计问题:
    • static startOffset = 0; 缺失类型定义,用int存储文件偏移在大文件场景会溢出,应改为long类型
    • WriteToFile方法内使用了await但没有加async关键字,Seek操作使用了未定义的offset变量,属于代码笔误
    • 偏移量自增使用ReaderWriterLock属于冗余设计,性能远差于原子操作,没有必要
  • 核心并发问题(直接导致乱码):
    你为每个线程单独创建了指向同一文件的FileStream实例,而**.NET默认的FileStream会开启4KB大小的用户态缓冲区**:
    1. 每个FileStream实例独立维护自己的缓冲区,写入10字节内容时不会立刻落盘,而是先存入缓冲区,直到缓冲区满、Stream被Dispose时,才会将整个缓冲区对应的文件块(默认4KB)整体写回磁盘
    2. 多线程并发运行时会出现典型的读-改-写竞态:比如线程A拿到偏移0,写入10字节到自己的缓冲区还没刷新;线程B拿到偏移10写入内容后先刷新缓冲区,把自己缓存的4KB块写回磁盘;后续线程A刷新缓冲区时,会把自己缓存的、未包含线程B写入内容的4KB块直接覆盖到磁盘,不仅会覆盖其他线程写入的内容,还会导致字节序列错乱
    3. 错乱的字节序列被Encoding.Default(双字节编码如GBK、Shift-JIS)解析时,就会出现日文字符、乱码符号
  • 其他隐性问题:
    • 创建FileStream时没有指定FileOptions.Asynchronous标志,此时的CopyToAsync是线程池调度的伪异步,无法利用重叠IO的偏移独立特性
    • 写入时没有对文件对应区域加排它锁,就算禁用缓冲区,Windows系统不保证无锁并发写入的原子性,可能出现写入撕裂
    • 多线程同时用FileMode.OpenOrCreate打开文件时,存在文件被意外截断的竞态窗口
修复方案

如果要实现多线程并行异步写文件,按以下规则修改代码:

  1. 偏移量计算改用原子操作,弃用冗余的读写锁
  2. 创建FileStream时禁用用户态缓冲区、开启异步IO标志,避免缓冲区刷新覆盖问题
  3. 写入前对目标文件区域加排它锁,写入完成后解锁
  4. 修正所有语法笔误

修复后的参考代码:

// 偏移量改用long类型避免溢出
static long startOffset = 0;
static int blockSize = 10;
static string fileName = "test.bin";

static async Task<long> WriteToFile(Stream dataToWrite)
{
    // 原子获取偏移并自增,无需读写锁
    long writeOffset = Interlocked.Add(ref startOffset, blockSize) - blockSize;
    // 缓冲区大小设为0禁用用户态缓冲,指定useAsync=true使用真异步IO
    using(var fs = new FileStream(fileName,
                FileMode.Open,
                FileAccess.ReadWrite,
                FileShare.ReadWrite,
                bufferSize: 0,
                useAsync: true))
    {
        // 锁定要写入的文件区域
        fs.Lock(writeOffset, blockSize);
        try
        {
            fs.Seek(writeOffset, SeekOrigin.Begin);
            await dataToWrite.CopyToAsync(fs);
            await fs.FlushAsync();
        }
        finally
        {
            // 确保异常时也能释放锁
            fs.Unlock(writeOffset, blockSize);
        }
    }
    return writeOffset;
}

// 调用入口
// 提前初始化创建文件、预分配大小,避免多线程打开竞态
using(var initFs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)){
    initFs.SetLength(4 * blockSize);
}
var tasks = new List<Task>();
for(int i=1;i<=4;i++)
{
    tasks.Add(Task.Run( async() => {
        string s = "aaaaaaaaaa";
        byte[] buffer = Encoding.Default.GetBytes(s);
        Stream data = new MemoryStream(buffer);
        long offset = await WriteToFile(data);
        Console.WriteLine($"Data written at offset - {offset}");
    }));
}

await Task.WhenAll(tasks);

如果追求更高写入性能,更优方案是用单线程专属负责文件写入,多个工作线程把待写入的内存块投递到线程安全队列,由写线程顺序写入,完全规避文件句柄的并发竞态,实际性能会比多句柄并发写更高。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 21:27:20