使用StreamWriter.WriteLineAsync结合WhenAll是否存在问题?
问题描述
我有一段基于.NET Standard 2.1的异步写入MemoryStream的代码,逻辑如下:
- 为每一行创建一个热任务;
- 等待所有任务执行完成;
- 最后将writer缓冲区刷新到
MemoryStream。
代码示例:
await using var memoryStream = new MemoryStream(); await using var writer = new StreamWriter(memoryStream); var recordTasks = stringRecordsToWrite.Select(r => writer.WriteLineAsync(r)); await Task.WhenAll(recordTasks); await writer.FlushAsync(); var result = memoryStream.ToArray();
我有两个疑问:
- 有反馈称偶尔会出现记录丢失的情况,该实现是否是问题根源?我本地尝试复现但未成功;
- Resharper提示SELECT语句中的writer是被捕获的变量且在外层作用域释放,这是否会引发问题?
或是这些都是误判,问题出在其他地方?
附尝试复现问题的代码(生成约250MB文件仍未复现问题):
internal class Program { public static async Task Main(string[] args) { var records = new Dictionary<string, IEnumerable<RecordsToWrite>>(); for (var i = 0; i < 200; i++) { var recordKey = $"test-{i}"; records.Add(recordKey, default); var itemRecords = new List<RecordsToWrite>(); for (var x = 0; x < 500; x++) { itemRecords.Add(new RecordsToWrite { Tracking = $"{recordKey}-Track-Ref-{x}" }); } records[recordKey] = itemRecords; } var resultAsBytes = new List<byte>(); var randomizer = new Random(); foreach (var kv in records) { await using var memoryStream = new MemoryStream(); await using var writer = new StreamWriter(memoryStream); var recordsToWrite = kv.Value; var writingRecordsTasks = recordsToWrite.Select(x => { var randomLengthString = randomizer.Next(100, 5000); return writer.WriteLineAsync($"track-ref-{x.Tracking}, " + $"now in ticks: {DateTime.UtcNow.Ticks}, " + $"content: {new string(GetRandomLetter(), randomLengthString)}"); }); await Task.WhenAll(writingRecordsTasks); await writer.FlushAsync(); resultAsBytes.AddRange(memoryStream.ToArray()); } var content = Encoding.UTF8.GetString(resultAsBytes.ToArray()); await File.WriteAllLinesAsync("PathToAFile.txt", new[] { content }); Console.ReadLine(); } public static char GetRandomLetter() => (char)('a' + new Random().Next(0, 26)); }
问题解答
关于记录丢失的疑问
你的代码实现确实存在引发记录丢失的风险,核心原因是**StreamWriter.WriteLineAsync并非线程安全方法**。当你用Select批量创建多个任务并通过Task.WhenAll并行执行时,多个WriteLineAsync调用会同时操作StreamWriter的内部缓冲区,这会导致缓冲区数据被覆盖、写入顺序混乱,极端情况下就会出现记录丢失的问题。
之所以本地难以复现,是因为并行操作的冲突概率和数据量、系统负载有关:小数据量下缓冲区操作速度快,冲突概率极低;只有在高并发、大数据量的场景下,才会大概率触发这类线程安全问题。
关于Resharper提示的疑问
Resharper的提示属于误判。你的代码中writer是用await using声明的,它的生命周期会覆盖到await Task.WhenAll(recordTasks)和await writer.FlushAsync()执行完成之后,不会在任务执行过程中被提前释放。任务捕获的writer变量在整个异步流程中都是有效的,不会引发对象已释放的异常。
修复建议
要解决记录丢失问题,必须保证StreamWriter的写入操作是串行执行的。你有两种可选方案:
- 放弃并行,串行写入:直接遍历
stringRecordsToWrite,逐个调用await writer.WriteLineAsync(r),这样能完全避免线程安全问题,代码更简单可靠。 - 先并行处理再串行写入:如果需要并行处理数据(比如先做数据转换),可以先并行处理所有记录得到结果集合,再串行写入
StreamWriter,既保留处理效率又避免写入冲突。
推荐方案一的代码示例:
await using var memoryStream = new MemoryStream(); await using var writer = new StreamWriter(memoryStream); foreach (var r in stringRecordsToWrite) { await writer.WriteLineAsync(r); } await writer.FlushAsync(); var result = memoryStream.ToArray();
内容的提问来源于stack exchange,提问作者AllmanTool
相关产品推荐
相关产品推荐

