如何优雅关闭多线程TcpListener避免ObjectDisposedException
TcpListener 优雅关闭方案(解决Stop触发ObjectDisposedException问题)
问题复现场景
- 基于.NET平台实现TcpListener服务端,启动流程无任何报错
- 无任何客户端接入的前提下,点击停止按钮调用
listener.Stop()方法时,抛出System.ObjectDisposedException第一次机会异常 - 复现路径仅需两步:启动监听器 → 点击停止监听器按钮,无其他额外操作
异常根因
异常并非Listener.Stop()方法本身抛出,而是调用BeginAcceptTcpClient后挂起的异步等待连接回调被强制触发导致:
- 调用
Listener.Start()后,BeginAcceptTcpClient会在底层线程池挂起一个等待客户端连接的异步任务 - 直接调用
Listener.Stop()会销毁底层监听Socket,同时强制完成所有挂起的异步接受请求 - 此时回调函数
AcceptClient被触发,执行Listener.EndAcceptTcpClient(ar)时会访问已经被释放的Socket实例,直接抛出对象已释放异常
原有代码的其他隐患
- 窗体级全局变量
cClient被所有异步回调共享,并发客户端接入时会出现实例覆盖问题 - 遍历
ClientList发送消息时直接在循环内移除元素,会导致索引错位、漏发消息 - 多线程访问
ClientList无锁保护,会出现集合修改枚举异常 - 停止监听器时未主动断开已连接的客户端,资源无法完全释放
xUpdate方法中使用On Error Resume Next会无差别吞掉所有运行错误,不利于问题排查
修复后完整实现
Imports System.IO, System.Net, System.Net.Sockets Public Class frmServer Dim Listener As TcpListener Dim ClientList As New List(Of ChatClient) ' 线程安全的停止标记 Private isListenerRunning As Boolean = False ' 客户端列表操作锁 Private ReadOnly clientLock As New Object() Sub xLoad() Handles Me.Load ' 启动前初始化运行状态 isListenerRunning = True Listener = New TcpListener(IPAddress.Any, 3818) Listener.Start() xUpdate("Server Started", False) Listener.BeginAcceptTcpClient(New AsyncCallback(AddressOf AcceptClient), Listener) End Sub Sub AcceptClient(ByVal ar As IAsyncResult) ' 先判断监听器状态,已停止则直接退出,避免访问已释放对象 If Not isListenerRunning Then Exit Sub Dim currentClient As ChatClient = Nothing Try ' 异步结束操作放在try块内,捕获停止时的预期异常 Dim tcpClientInstance = Listener.EndAcceptTcpClient(ar) currentClient = New ChatClient(tcpClientInstance) AddHandler currentClient.MessageRecieved, AddressOf MessageRecieved AddHandler currentClient.ClientExited, AddressOf ClientExited ' 加锁操作客户端列表,避免多线程冲突 SyncLock clientLock ClientList.Add(currentClient) End SyncLock xUpdate("New Client Joined", True) ' 仅在监听器运行状态下继续投递下一个接受请求 If isListenerRunning Then Listener.BeginAcceptTcpClient(New AsyncCallback(AddressOf AcceptClient), Listener) End If Catch ex As ObjectDisposedException ' 监听器停止触发的预期异常,直接忽略 Exit Sub Catch ex As SocketException ' 停止时底层Socket释放触发的异常,直接忽略 Exit Sub Catch ex As Exception ' 其他业务异常可按需记录日志 xUpdate($"Accept client error: {ex.Message}", False) End Try End Sub Sub MessageRecieved(ByVal Str As String) xUpdate(Str, True) End Sub Sub ClientExited(ByVal Client As ChatClient) SyncLock clientLock ClientList.Remove(Client) End SyncLock xUpdate("Client Exited", True) End Sub Delegate Sub _xUpdate(ByVal Str As String, ByVal Relay As Boolean) Sub xUpdate(ByVal Str As String, ByVal Relay As Boolean) If InvokeRequired Then Invoke(New _xUpdate(AddressOf xUpdate), Str, Relay) Else txtMainTextBox.AppendText(Str & vbNewLine) If Relay Then Send(Str) End If End Sub Private Sub TextBox2_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles txtSendTextBox.KeyDown End Sub Sub Send(ByVal Str As String) ' 先复制客户端列表快照,避免遍历过程中集合修改导致异常 Dim clientSnapshot As New List(Of ChatClient) SyncLock clientLock clientSnapshot.AddRange(ClientList) End SyncLock For Each client In clientSnapshot Try client.Send(Str) Catch SyncLock clientLock If ClientList.Contains(client) Then ClientList.Remove(client) End SyncLock End Try Next End Sub Private Sub btnSend_Click(sender As Object, e As EventArgs) Handles btnSend.Click If Me.txtSendTextBox.Text = "" Then Exit Sub End If xUpdate("Server Says: " & Me.txtSendTextBox.Text, True) txtSendTextBox.Clear() End Sub Private Sub btnStopListener_Click(sender As Object, e As EventArgs) Handles btnStopListener.Click ' 先修改停止标记,阻止后续异步请求投递 isListenerRunning = False Try ' 停止监听端口 Listener.Stop() Catch ' 忽略停止过程中的预期异常 End Try ' 主动断开所有已连接客户端,释放资源 SyncLock clientLock For Each client In ClientList Try ' 需在ChatClient类中实现Close方法,完成内部TcpClient、流资源的释放 client.Close() Catch End Try Next ClientList.Clear() End SyncLock xUpdate("Server Stopped", False) End Sub End Class
关键修复点说明
- 新增
isListenerRunning布尔标记,停止时优先修改状态,回调入口先判断标记,拦截无效操作 - 所有
EndAcceptTcpClient操作放在异常捕获块内,主动忽略监听器停止时触发的预期异常 - 移除全局共享的
cClient变量,改为回调内局部变量存储当前客户端实例,避免并发场景下实例覆盖 - 所有客户端列表操作加锁保护,发送消息时先拷贝列表快照再遍历,避免集合修改异常
- 停止监听器时主动断开所有已连接客户端,完成全链路资源释放
内容的提问来源于stack exchange,提问作者Andrew
相关产品推荐
相关产品推荐

