将Pcap.Net的ReceivePackets封装进线程/任务等引发访问冲突异常
如何将Pcap.Net的ReceivePackets方法封装到线程/Task/BackgroundWorker中?
我尝试用Thread/Task/BackgroundWorker封装Pcap.Net库的ReceivePackets方法,但抛出了System.AccessViolationException: 尝试读取或写入受保护的内存错误,以下是我的代码:
线程创建代码
PacketDevice selectedDevice = allDevices[deviceIndex - 1]; // Open the device using (communicator = selectedDevice.Open(65536, // 捕获的数据包部分长度 // 65536确保在所有链路层都能捕获完整数据包 PacketDeviceOpenAttributes.Promiscuous, // 混杂模式 1000)) // 读取超时时间 { Console.WriteLine("Listening on " + selectedDevice.Description + "..."); // 启动捕获线程 pThread.Start(); }
recvP函数与数据包处理函数
private void recvP() { communicator.ReceivePackets(0, PacketHandler); } // Pcap.Net收到每个数据包时调用的回调函数 private void PacketHandler(Packet packet) { UdpDatagram datagram = packet.Ethernet.IpV4.Udp; if (datagram != null && datagram.SourcePort == 6969) { MemoryStream stream = datagram.Payload.ToMemoryStream(); byte[] byteStream = stream.ToArray(); current_packet_id = BitConverter.ToUInt16(byteStream, 0); Console.WriteLine("Got chunk number: " + current_packet_id); if (current_packet_id < last_packet_id) // 新图像(图像的第一个数据包) { if (!firstImage) ShowImage(); // 如果前一张图像的所有数据包都已收到,显示图像 data.Clear(); // 清空之前图像的所有数据 data.AddRange(byteStream.Skip(2).Take(byteStream.Length - 2).ToList()); } else // 同一张图像的后续数据包 data.AddRange(byteStream.Skip(2).Take(byteStream.Length - 2).ToList()); last_packet_id = current_packet_id; } }
问题原因
错误核心是communicator对象的生命周期和线程执行不同步:using块会在代码执行完毕后自动释放communicator,但你在using块内启动线程后,线程还在后台执行ReceivePackets,此时communicator已经被销毁,线程尝试访问已释放的内存就会触发AccessViolationException。
正确的封装方式
1. 使用Thread(手动管理对象生命周期)
把using块替换为手动释放逻辑,确保communicator在线程执行完毕后再销毁:
PacketDevice selectedDevice = allDevices[deviceIndex - 1]; // 不用using,手动控制资源释放时机 communicator = selectedDevice.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 1000); try { Console.WriteLine("Listening on " + selectedDevice.Description + "..."); pThread = new Thread(recvP); pThread.Start(); pThread.Join(); // 等待线程执行完毕,再执行资源释放 } finally { // 确保无论线程是否异常,都释放资源 communicator?.Dispose(); }
2. 使用Task异步处理
利用await等待任务完成,保证using块在任务结束后才释放communicator:
async Task StartCaptureAsync() { PacketDevice selectedDevice = allDevices[deviceIndex - 1]; using (var communicator = selectedDevice.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 1000)) { Console.WriteLine("Listening on " + selectedDevice.Description + "..."); // 在Task中执行捕获逻辑,await确保任务完成后再退出using块 await Task.Run(() => communicator.ReceivePackets(0, PacketHandler)); } }
3. 使用BackgroundWorker(WinForms/WPF场景)
在后台线程的DoWork事件中完整处理捕获逻辑,确保communicator在事件执行期间有效:
private void StartCaptureWithBackgroundWorker() { PacketDevice selectedDevice = allDevices[deviceIndex - 1]; var worker = new BackgroundWorker(); worker.DoWork += (sender, e) => { // 在后台线程内创建并使用communicator,生命周期覆盖整个捕获过程 using (var communicator = selectedDevice.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 1000)) { communicator.ReceivePackets(0, PacketHandler); } }; worker.RunWorkerCompleted += (sender, e) => { // 捕获结束后的清理或通知逻辑 Console.WriteLine("Capture finished."); }; worker.RunWorkerAsync(); }
额外注意事项
- 线程安全:
data、current_packet_id、last_packet_id是多线程共享变量,必须加锁避免竞态条件:
private readonly object _lock = new object(); private void PacketHandler(Packet packet) { UdpDatagram datagram = packet.Ethernet.IpV4.Udp; if (datagram != null && datagram.SourcePort == 6969) { MemoryStream stream = datagram.Payload.ToMemoryStream(); byte[] byteStream = stream.ToArray(); lock(_lock) // 加锁保护共享变量 { current_packet_id = BitConverter.ToUInt16(byteStream, 0); Console.WriteLine("Got chunk number: " + current_packet_id); if (current_packet_id < last_packet_id) { if (!firstImage) ShowImage(); data.Clear(); data.AddRange(byteStream.Skip(2).Take(byteStream.Length - 2).ToList()); } else data.AddRange(byteStream.Skip(2).Take(byteStream.Length - 2).ToList()); last_packet_id = current_packet_id; } } }
- UI线程更新:如果
ShowImage是更新UI的操作,WinForms中需要用Invoke切换到UI线程,WPF用Dispatcher.Invoke,否则会触发跨线程访问UI的错误。
内容的提问来源于stack exchange,提问作者Yonka2019
相关产品推荐
相关产品推荐

