多线程直接写同一文件会引发IOException或内容错乱,因操作系统限制文件句柄并发写入且.NET文件API非线程安全;推荐按频率选择lock串行、ConcurrentQueue批量异步或复用FileStream加锁写入。

多线程直接写同一个文件会出什么问题
多个线程同时调用 File.WriteAllText、StreamWriter 或 FileStream.Write 写同一路径,大概率触发 IOException(如“该进程无法访问该文件,因为另一进程正在使用此文件”),或更隐蔽的写入错乱——内容被截断、覆盖、字节交错。这不是 .NET 的 bug,而是操作系统对文件句柄共享的限制:默认不允许多个写入者并发操作同一文件句柄,且 .NET 的文件 API 大多不是线程安全的。
用 lock + 单一 StreamWriter 实现串行写入
最简单可控的方式是把写操作“排队”。关键不是锁整个方法,而是锁一个**静态对象**,并确保所有写入都走同一个线程安全的出口:
private static readonly object _fileLock = new object();
private static readonly string _logPath = "app.log";
public static void AppendLog(string message)
{
lock (_fileLock)
{
File.AppendAllText(_logPath, $"[{DateTime.Now:HH:mm:ss}] {message}\n");
}
}
-
File.AppendAllText内部每次打开/关闭文件,适合低频写入;高频场景下开销大,可能成为瓶颈 - 必须用
static锁对象,否则实例锁无效 - 不要锁
typeof(YourClass)或字符串字面量,容易引发死锁或意外争用
高频写入用 ConcurrentQueue + 后台线程批量落盘
当每秒写入几十次以上,lock 会阻塞大量线程。改用生产者-消费者模式:
private static readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>();
private static readonly CancellationTokenSource _cts = new CancellationTokenSource();
static LogWriter()
{
Task.Run(() => WriteLoopAsync(_cts.Token));
}
private static async Task WriteLoopAsync(CancellationToken ct)
{
var buffer = new List<string>(1024);
while (!ct.IsCancellationRequested)
{
if (_logQueue.TryDequeue(out var msg))
{
buffer.Add(msg);
if (buffer.Count >= 1000)
{
await FlushBufferAsync(buffer);
buffer.Clear();
}
}
else
{
await Task.Delay(10, ct); // 避免空转
}
}
if (buffer.Count > 0) await FlushBufferAsync(buffer);
}
private static async Task FlushBufferAsync(List<string> buffer)
{
await File.AppendAllLinesAsync(_logPath, buffer); // 异步落盘,不阻塞主线程
}
-
ConcurrentQueue无锁,支持高并发入队 - 批量写入减少磁盘 I/O 次数,比单条
AppendAllText快数倍 -
File.AppendAllLinesAsync是原子的,但注意它仍会打开/关闭文件;若需极致性能,可复用FileStream并加锁控制写入点
用 FileStream 复用句柄 + lock 控制写入位置
适用于需要严格顺序、低延迟、且能接受稍复杂管理的场景(如实时日志、传感器数据流):
private static FileStream _logStream;
private static readonly object _streamLock = new object();
static LogWriter()
{
_logStream = new FileStream("data.bin", FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.Asynchronous);
}
public static void WriteBinary(ReadOnlySpan<byte> data)
{
lock (_streamLock)
{
_logStream.Write(data);
_logStream.Flush(); // 确保立即落盘,避免缓存延迟
}
}
- 必须显式
Flush(),否则数据可能滞留在内核缓冲区 -
FileShare.Read允许其他进程读取,但禁止其他写入者打开同一文件 - 程序退出前必须
_logStream?.Dispose(),否则文件句柄泄漏 - 不要在
lock块里做耗时操作(如网络调用、复杂计算),否则拖慢所有写入线程
真正难的不是选哪种方案,而是判断你的写入频率、数据重要性、是否允许丢日志、能否接受延迟——这些决定了该用 lock、队列还是复用流。别等线上出问题才看 IOException 堆栈。










