异步编程踩坑——服务端停止阻塞
在编写程序的通讯模块时,涉及到了TCP通讯。模块的设计思路是,开启TCP端服务器后在一个线程中实时监听端口号,为每一个连接进来的客户端分配一个线程用来实时通信。
服务端在关闭时通过将标志位取反实现线程的结束,从而关闭服务端。
下面这段代码看着没什么问题,但关键就在于this.ServerSocket.Accept();,这个方法会阻塞当前线程。
点击查看代码
public void StartSocketListening()
{
try
{
while (this.IsStartListening)
{
//有客户端链接 就接受,此处会直接阻塞当前线程 可能导致线程无法及时终端。
System.Net.Sockets.Socket socket = this.ServerSocket.Accept();
try
{
Thread.Sleep(100);
//添加到 队列中 进行管理
this.ClientSocketList.Add(socket);
//获取端口号 和IP 地址
string text = ((IPEndPoint)socket.RemoteEndPoint).Address.ToString();
string text2 = ((IPEndPoint)socket.RemoteEndPoint).Port.ToString();
//通过委托 把链接进来的客户端 信息返回出去
this.OnTcpServerStateInfoEnterHead(string.Concat(new string[]
{
"<",
text,
":",
text2,
">---上线"
}), SocketState.ClientOnline);
//返回当前客户端 对应的socket
this.OnTcpServerOnlineClientEnterHead(socket);
//传递 当前客户端数量
this.OnTcpServerReturnClientCountEnterHead(this.ClientSocketList.Count);
//在线程池中 异步调用这个方法(ClientSocketCallBack),并把客户端的socket作为变量传入ClientSocketCallBack
ThreadPool.QueueUserWorkItem(new WaitCallback(this.ClientSocketCallBack), socket);
Log.Info($"{text} 客户端已经连接");
}
catch (Exception)
{
//异常则关闭连接 移除客户端
socket.Shutdown(SocketShutdown.Both);
this.OnTcpServerOfflineClientEnterHead(socket);
this.ClientSocketList.Remove(socket);
}
}
}
catch (Exception ex)
{
this.OnTcpServerErrorMsgEnterHead(ex.Message);
}
}
接着看下面关闭服务端时的逻辑处理。这段代码看着没什么问题,通过设置标志位为false,让上面的线程退出循环,然后通过Interrupt和Join打断线程后等待其结束回收资源。没有采用Abort()是因为dotnet早以不推荐这种方式进行线程退出,替代方法是通过Token抛出异常信息来强制打断线程执行。。
回到下面的代码,我没有采用强制方法终端线程执行,这就导致即便我将标志位设为false,线程依旧因为Accept()方法处于阻塞状态。,更糟糕的问题是,我后面还调用了Join方法,这个方法直接阻塞了主线程。具体表现就是,每当我关闭TCP服务端,整个程序直接卡死。
替代方法
void ProcessPendingWorkItemsNew(CancellationToken cancellationToken)
{
if (QueryIsMoreWorkPending())
{
// If the CancellationToken is marked as "needs to cancel",
// this will throw the appropriate exception.
cancellationToken.ThrowIfCancellationRequested();
WorkItem work = DequeueWorkItem();
ProcessWorkItem(work);
}
}
服务端关闭
public void Stop()
{
try
{
this.IsStartListening = false;
if (this.StartSockst != null)
{
//关闭监听客户端的线程
this.StartSockst.Interrupt();
//等待线程完全结束 回收资源
this.StartSockst.Join();
}
if (this.ServerSocket != null)
{
this.ServerSocket.Shutdown(SocketShutdown.Both);
this.ServerSocket.Close();
}
//传出 服务端数据
this.OnTcpServerStateInfoEnterHead(string.Format("服务端Ip:{0},端口:{1}已停止监听", this.ServerIp, this.ServerPort), SocketState.StopListening);
for (int i = 0; i < this.ClientSocketList.Count; i++)
{
this.OnTcpServerOfflineClientEnterHead(this.ClientSocketList[i]);
//关闭客户端链接
this.ClientSocketList[i].Shutdown(SocketShutdown.Both);
//关闭套接字 释放资源
this.ClientSocketList[i].Close();
}
// GC.Collect();
}
catch (SocketException)
{
}
}
通过上面的分析,问题已经十分清晰了。重要的是避免线程的阻塞,所以调整为如下的代码逻辑即可顺利取消线程。我们直接关闭TCP的Socket,由于线程调用了Accept()方法,因此该线程会报错,然后就能顺利取消线程。
正确的方法
public void Stop()
{
try
{
this.IsStartListening = false;
if (this.StartSockst != null)
{
//如果在此处 我们不先关闭 服务端的socket 那么在对应线程的循环中
//调用的accept方法会一直阻塞,导致IsStartListening不能即使中止方法。(记一下这里)
//关闭socket让线程中的accept方法 强制抛出异常 结束执行
if (this.ServerSocket != null)
{
//服务端不需要调用该方法
//this.ServerSocket.Shutdown(SocketShutdown.Both);
this.ServerSocket.Close();
}
//关闭监听客户端的线程
this.StartSockst.Interrupt();
//this.StartSockst.Abort();
//等待线程完全结束 回收资源
this.StartSockst.Join();
}
//传出 服务端数据
this.OnTcpServerStateInfoEnterHead(string.Format("服务端Ip:{0},端口:{1}已停止监听", this.ServerIp, this.ServerPort), SocketState.StopListening);
for (int i = 0; i < this.ClientSocketList.Count; i++)
{
this.OnTcpServerOfflineClientEnterHead(this.ClientSocketList[i]);
//关闭客户端链接
this.ClientSocketList[i].Shutdown(SocketShutdown.Both);
//关闭套接字 释放资源
this.ClientSocketList[i].Close();
}
// GC.Collect();
}
catch (SocketException)
{
}
}
最后,我再补充一个更好的方法。Accept()还有一个异步版本AcceptAsync()方法,我们直接在当前通讯类中创建一个Cts,将其作为循环监听以及AcceptAsync()的控制信号,这样当我们需要取消通讯时,直接调用Cancel()方法,就能同时取消通讯以及退出循环。
2025.9.10补充
更深入的解释: Interrupt()本意是通过抛出异常的方式,打断处于阻塞状态的线程的。我这里的问题,实际上是直接出现死锁了。由于Join()导致主线程被阻塞。Interupt能够打断的线程的前提条件是处于以下几种状态时(Wait、Sleep、Join),但是调用Accept的阻塞,并不会让线程处于这几种状态中,因此导致了死锁。


浙公网安备 33010602011771号