为何StreamWriter异步写入时偶发NullReferenceException且未捕获?
我在开发CAN总线数据记录功能时,通过FrameReceived方法将接收的CAN帧存入列表,再调用StartRecord/StopRecord触发文件写入流程。目前遇到偶发的NullReferenceException,异常指向await writer.WriteLineAsync(line)代码行,但已确认line非空、writer初始化正常,同时疑惑为何异常未被内部try-catch捕获,以及空引用的对象到底是什么。
数据收集相关代码
private List<string> recordList = new List<string>(); private List<string> recordList1 = new List<string>(); private List<string> recordList2 = new List<string>(); public void FrameReceived(CANFrame frame) { if (!isRecord) return; if (frame != null) { string record = RecordFrame(frame, "Rx"); recordList.Add(record); if (frame.Channel == 1) recordList1.Add(record); if (frame.Channel == 2) recordList2.Add(record); } } public string RecordFrame(CANFrame frame, string type) { string text1 = string.Empty; if (frame.type == TPCANMessageType.PCAN_MESSAGE_STANDARD) { text1 = ($"{stopwatch.ElapsedTicks * 1.0 / Stopwatch.Frequency,11:#0.000000} {frame.Channel} {frame.Id,-15:X} {type} d {frame.Length} {frame.ToHexString()}"); } else if (frame.type == TPCANMessageType.PCAN_MESSAGE_EXTENDED) { text1 = ($"{stopwatch.ElapsedTicks * 1.0 / Stopwatch.Frequency,11:#0.000000} {frame.Channel} " + $"{frame.Id:X}x".PadRight(15) + $" {type} d {frame.Length} {frame.ToHexString()}"); } else if (frame.type == TPCANMessageType.PCAN_MESSAGE_FD) { text1 = ($"{stopwatch.ElapsedTicks * 1.0 / Stopwatch.Frequency:#0.000000} CANFD {frame.Channel} {type} {frame.Id:X} 0 0 1 {frame.DLC:X2} {frame.Length} {frame.ToHexString()} 0 0 39b5 0 0 0 0"); } else { if (frame.Data.Length <= 8) { text1 = ($"{stopwatch.ElapsedTicks * 1.0 / Stopwatch.Frequency,11:#0.000000} {frame.Channel} " + (frame.Id > 0x7FF ? $"{frame.Id:X}x".PadRight(15) : $"{frame.Id,-15:X}") + $" {type} d {frame.Length} {frame.ToHexString()}"); } else { text1 = ($"{stopwatch.ElapsedTicks * 1.0 / Stopwatch.Frequency:#0.000000} CANFD {frame.Channel} {type} " + (frame.Id > 0x7FF ? $"{frame.Id:X}x" : $"{frame.Id:X}") + $" 0 0 1 {frame.DLC:X2} {frame.Length} {frame.ToHexString()} 0 0 39b5 0 0 0 0"); } } return text1; }
记录启停与文件写入代码
public void StartRecord(string fileName) { this.fileName = fileName; stopwatch.Reset(); stopwatch.Start(); recordList.Clear(); recordList1.Clear(); recordList2.Clear(); isRecord = true; } public void StopRecord() { isRecord = false; WriteToFile(); stopwatch.Stop(); } private async void WriteToFile() { try { Task writeAll = WriteToFileAsync($"{fileName}\\合并CAN.asc", new List<string>(recordList)); Task write1 = WriteToFileAsync($"{fileName}\\内CAN.asc", new List<string>(recordList1)); Task write2 = WriteToFileAsync($"{fileName}\\外CAN.asc", new List<string>(recordList2)); await Task.WhenAll(writeAll, write1, write2); } catch(Exception ex) { SmartFrameWork.Console.WriteLine("WriteToFile()"); SmartFrameWork.Console.WriteLine(ex.ToString()); } } async Task WriteToFileAsync(string fileName,List<string> lines) { try { using (StreamWriter writer = new StreamWriter(fileName)) { await writer.WriteLineAsync($"date {DateTime.Now.ToString("dd/M/yyyy HH:mm:ss.fff")}"); await writer.WriteLineAsync("base hex timestamps absolute"); await writer.WriteLineAsync("no internal events logged"); await writer.WriteLineAsync("//Version 5.6"); foreach (var line in lines) { if (writer == null) SmartFrameWork.Console.WriteLine("writer为空"); if (line == null) SmartFrameWork.Console.WriteLine("写入该行的内容为空"); await writer.WriteLineAsync(line); } await writer.FlushAsync(); } } catch(Exception ex) { SmartFrameWork.Console.WriteLine("WriteToFileAsync(string fileName,List<string> lines)"); SmartFrameWork.Console.WriteLine(ex.ToString()); } }
异常信息
System.NullReferenceException:未将对象引用设置到对象的实例。 在
System.IO.StreamWriter.d_55.MoveNext()
---引发异常的上一位置中堆栈跟踪的末尾· 在
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task
task) 在
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task
task) 在 System.Runtime.CompilerServices.TaskAwaiter.GetResult() 在
CANBusDriver.FileFrameFilter.d_35.MoveNext() 位置
D:\Projects\HIL\HIL_fanqingyu\V5.6Beta\HIL\CANdriver\FileFrameFilter.cs:行号157
--引发异常的上一位置中堆栈跟踪的末尾-- 在
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task
task) 在
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task
task) 在 System.Runtime.CompilerServices.TaskAwaiter.GetResult() 在
CANBusDriver.FileFrameFilter.d_34.MoveNext() 位置
D:\Projects\HIL\HIL_fanqingyu\V5.6Beta\HIL\CANdriver\FileFrameFilter.cs:行号 145
--引发异常的上一位置中堆栈跟踪的末尾-- 在
System.Runtime.CompilerServices.AsyncMethodBuilderCore.<>c.b__6_1(Object
state) 在 System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state) 在
System.Threading.ExecutionContext.RunInternal(ExecutionContext
executionContext, ContextCallback callback, Object state, Boolean
preserveSyncCtx) 在
System.Threading.ExecutionContext.Run(ExecutionContext
executionContext,ContextCallback callback, Object state, Boolean
preserveSyncCtx) 在
System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
在 System.Threading.ThreadPoolWorkQueue.Dispatch() 在
System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
问题分析与解决方案
1. 异常未被捕获的原因
WriteToFile是async void方法,这类方法的异常无法被上层try-catch正确捕获,会直接抛到调用上下文的同步上下文,导致异常逃逸。此外,异步操作的上下文切换可能导致StreamWriter内部异常在using块释放后触发,绕过当前方法的try-catch。
2. 空引用的可能对象
StreamWriter底层流:文件系统异常(如文件被占用、磁盘空间不足、权限变更)可能导致底层流被意外关闭,此时StreamWriter内部引用变为null,触发空引用异常。- 异步状态失效:
async void方法无返回任务,无法跟踪异步操作状态,可能导致StopRecord中的stopwatch.Stop()与文件写入操作并发,间接破坏StreamWriter的内部状态。
3. 修复建议
(1)将async void改为async Task并等待
修改方法签名,确保异步操作的异常能被正确捕获:
public async Task StopRecord() { isRecord = false; await WriteToFile(); stopwatch.Stop(); } private async Task WriteToFile() { try { Task writeAll = WriteToFileAsync($"{fileName}\\合并CAN.asc", new List<string>(recordList)); Task write1 = WriteToFileAsync($"{fileName}\\内CAN.asc", new List<string>(recordList1)); Task write2 = WriteToFileAsync($"{fileName}\\外CAN.asc", new List<string>(recordList2)); await Task.WhenAll(writeAll, write1, write2); } catch(Exception ex) { SmartFrameWork.Console.WriteLine("WriteToFile()"); SmartFrameWork.Console.WriteLine(ex.ToString()); } }
(2)增加文件操作防御与异常细化
在WriteToFileAsync中增加前置检查,细化异常捕获:
async Task WriteToFileAsync(string fileName,List<string> lines) { try { // 确保目录存在 var dir = Path.GetDirectoryName(fileName); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } // 检查文件是否可写入 if (File.Exists(fileName) && (File.GetAttributes(fileName) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) { SmartFrameWork.Console.WriteLine($"文件只读无法写入:{fileName}"); return; } using (StreamWriter writer = new StreamWriter(fileName)) { await writer.WriteLineAsync($"date {DateTime.Now.ToString("dd/M/yyyy HH:mm:ss.fff")}"); await writer.WriteLineAsync("base hex timestamps absolute"); await writer.WriteLineAsync("no internal events logged"); await writer.WriteLineAsync("//Version 5.6"); foreach (var line in lines) { // 空行防御 if (string.IsNullOrWhiteSpace(line)) { SmartFrameWork.Console.WriteLine("跳过空行写入"); continue; } // 流状态检查 if (writer == null || writer.BaseStream == null || !writer.BaseStream.CanWrite) { SmartFrameWork.Console.WriteLine($"写入流已失效:{fileName}"); break; } await writer.WriteLineAsync(line); } await writer.FlushAsync(); } } catch(IOException ioEx) { SmartFrameWork.Console.WriteLine($"文件IO异常:{fileName}"); SmartFrameWork.Console.WriteLine(ioEx.ToString()); } catch(NullReferenceException nrEx) { SmartFrameWork.Console.WriteLine($"空引用异常:{fileName}"); SmartFrameWork.Console.WriteLine(nrEx.ToString()); } catch(Exception ex) { SmartFrameWork.Console.WriteLine($"写入文件异常:{fileName}"); SmartFrameWork.Console.WriteLine(ex.ToString()); } }
(3)临时用同步写入验证
若异步问题难以排查,可临时改用同步写入,验证是否为异步上下文导致的问题:
void WriteToFileSync(string fileName,List<string> lines) { try { var dir = Path.GetDirectoryName(fileName); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); using (StreamWriter writer = new StreamWriter(fileName)) { writer.WriteLine($"date {DateTime.Now.ToString("dd/M/yyyy HH:mm:ss.fff")}"); writer.WriteLine("base hex timestamps absolute"); writer.WriteLine("no internal events logged"); writer.WriteLine("//Version 5.6"); foreach (var line in lines) { if (!string.IsNullOrWhiteSpace(line)) { writer.WriteLine(line); } } writer.Flush(); } } catch(Exception ex) { SmartFrameWork.Console.WriteLine($"同步写入异常:{fileName}"); SmartFrameWork.Console.WriteLine(ex.ToString()); } }
内容的提问来源于stack exchange,提问作者Lan

