Unity中Socket.ConnectAsync成功连接后程序无限挂起问题
原本在控制台应用中运行正常的简易TCP客户端/服务端,将客户端代码移植到Unity后,调用socket.ConnectAsync并使用.Wait()阻塞等待连接时,程序出现无限挂起。即使服务器已接受连接(服务器日志已记录),Unity编辑器仍无法恢复正常。
控制台应用中无此问题,使用阻塞式socket.Connect替代异步方法时,可实现预期的阻塞至连接成功后继续执行的行为。Unity版本为2022.3.12f1。
客户端类代码
public class TCPClient { private ArraySegment<byte> buffer; private Socket? socket; public delegate void onReceive_delegate(int bytesReceived, EndPoint endpoint); public onReceive_delegate? onReceive; public TCPClient(ArraySegment<byte> buffer) { this.buffer = buffer; } private void Open() { if (socket != null) Dispose(); socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); } public async Task Connect(EndPoint remoteEP) { Open(); await socket!.ConnectAsync(remoteEP); // 此处导致挂起 _ = Listen(); } private async Task Listen() { if (socket == null) return; int receivedBytes = await socket.ReceiveAsync(buffer, SocketFlags.None); onReceive?.Invoke(receivedBytes, socket.RemoteEndPoint!); _ = Listen(); } public void Disconnect() { Dispose(); } public void Dispose() { if (socket == null) return; socket.Dispose(); socket = null; } }
执行连接的代码
public class Test : MonoBehaviour { void Start() { byte[] buffer = new byte[1024]; TCPClient client = new TCPClient(buffer); // _ = client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 54892)); // ^=> 此写法不会导致编辑器挂起,符合预期 client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 54892)).Wait(); // ^=> 导致编辑器无限挂起 } }
Unity主线程采用单线程同步上下文,当调用.Wait()阻塞主线程时,await socket.ConnectAsync完成后会尝试回到原同步上下文(即被阻塞的主线程)继续执行后续代码,但主线程此时被.Wait()卡死,无法处理异步回调,最终造成死锁,表现为程序无限挂起。
而控制台应用的同步上下文是线程池上下文,await完成后可直接在线程池线程继续执行,不会出现死锁。
方案1:使用同步连接(简单直接)
如果必须阻塞至连接完成,直接使用阻塞式socket.Connect方法,这是Unity环境下最稳妥的同步连接方式:
public void Connect(EndPoint remoteEP) { Open(); socket!.Connect(remoteEP); // 同步阻塞连接 _ = Listen(); }
注意:同步连接会阻塞主线程,若连接超时会导致编辑器/游戏卡顿,建议添加超时逻辑(比如用Socket.Poll配合连接尝试)。
方案2:避免阻塞主线程(推荐)
放弃使用.Wait(),改用Unity支持的异步生命周期方法,将Start改为async void:
public class Test : MonoBehaviour { async void Start() { byte[] buffer = new byte[1024]; TCPClient client = new TCPClient(buffer); await client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 54892)); // 连接成功后的逻辑在此处执行 } }
这种方式不会阻塞主线程,符合Unity的异步编程规范。
方案3:修改异步方法避免回到原上下文
如果一定要保留.Wait(),可在await时添加ConfigureAwait(false),让异步完成后不回到Unity主线程,直接在线程池线程执行后续代码:
public async Task Connect(EndPoint remoteEP) { Open(); await socket!.ConnectAsync(remoteEP).ConfigureAwait(false); // 跳过原同步上下文 _ = Listen(); }
注意:后续的Listen方法也会在线程池线程执行,若需要操作Unity UI或场景对象,需通过UnityMainThreadDispatcher等工具切换回主线程。
内容的提问来源于Stack Exchange,提问作者匿名

