You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优雅关闭多线程TcpListener避免ObjectDisposedException

TcpListener 优雅关闭方案(解决Stop触发ObjectDisposedException问题)

问题复现场景

  • 基于.NET平台实现TcpListener服务端,启动流程无任何报错
  • 无任何客户端接入的前提下,点击停止按钮调用listener.Stop()方法时,抛出System.ObjectDisposedException第一次机会异常
  • 复现路径仅需两步:启动监听器 → 点击停止监听器按钮,无其他额外操作

异常根因

异常并非Listener.Stop()方法本身抛出,而是调用BeginAcceptTcpClient后挂起的异步等待连接回调被强制触发导致:

  1. 调用Listener.Start()后,BeginAcceptTcpClient会在底层线程池挂起一个等待客户端连接的异步任务
  2. 直接调用Listener.Stop()会销毁底层监听Socket,同时强制完成所有挂起的异步接受请求
  3. 此时回调函数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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 09:33:39