线程切换
本库大部分消息处理建议是基于插件形式,是一个Task任务,运行在多线程上,在unity使用时,部分场景下需要把数据交回主线程处理。
但是,TouchSocket的插件系统是支持调用中断的,也可以让调用链上的所有前序处理者知道后续消息是否处理过了:
比如这条消息我没法处理,我调用await e.InvokeNext()等待下一个处理者,如果下一个处理者处理了,设置e.Handled = true,上一级通过获取这个值,就知道后续处理者有没有处理过了;
同时如果设置了e.Handled = true,再调用await e.InvokeNext(),后续也不会处理,代码内部会判断这个值;
调用retun || return EasyTask.CompletedTask (一般直接retun即可)调用链结束,然后从后往前结束await。
从中可以知道,调用await e.InvokeNext()并不是不管这条数据了,而是等待后续处理结果,再执行我后面的逻辑;同时如果我处理完了,后续的可以不用再执行,避免不必要的性能浪费。
所以,这一块没有一个标准的处理方式,仁者见仁智者见智,多参考官方文档,来写处理方式。基于本特性,建议使用UniTask的await UniTask.SwitchToMainThread();和await UniTask.SwitchToThreadPool();方式来切换上下文所处线程。
但是注意一点:
UniTask.SwitchToThreadPool()不会返回原本线程,这个问过AI,倒也没啥影响,即使上下文不处在同一个线程下,也不会影响上下文,不过有一个UniTask.SwitchToSynchronizationContext(),测试下来没啥效果,上下文线程ID还是不一致。
基于上面的内容,这里给出三种处理方式:
写一个Unity主线程调度器,通过协程或者unity生命周期函数定时调用执行
通过事件广播,只把内容传出去,让具体接收者处理,不管插件系统上下文
上面这两种,在基础依赖这篇文章中都有对应的模块实现,对应的函数为:UnityMainThreadDispatcher.Enqueue(Action action)和ModuleManager.GetModule<EventManager>().Fire(EventArgsBase eventArgs);
https://pwaki08fkw.feishu.cn/sync/PsbNd18Z0s7LSbbqDJscucC9nKf
使用
await UniTask.SwitchToMainThread();和await UniTask.SwitchToThreadPool();方式来切换上下文所处线程,
//其他线程
await UniTask.SwitchToMainThread();
//主线程上下文
//在此可以调用unity相关内容
await UniTask.SwitchToThreadPool();
//切回线程池
//后续处理逻辑Unity下其他注意内容
可以前往官方文档查看注意事项以及解决方案。
https://touchsocket.net/docs/troubleshootunity3d
其中主要的就是,因为库大部分使用了动态调用,部分情况下,Unity不支持,尤其是在IL2CPP下,建议优先使用Mono。如果必须使用LI2CPP(HybridCLR代码热更方案本条必须),参考官方文档进行设置优化(出问题再管)。针对比较常用的插件,改用委托接收;委托也可以实现await e.InvokeNext()进行链式调用和等待
异常捕获
unity下的Task以及UniTask在其作用域下发生异常,如果没有做捕获,会导致task无限期等待以及没有日志返回(据说是在程序结束时才会统一返回)
解决方案可以询问AI获得,下面是AI返回的解决方案:(基于Task,Unitask应该是一样的)
在异步方法中用
await Task.Run(...);,或同步代码中用task.Wait();,确保异常能向上传递。(task.Wait();会在同步代码中堵塞,会有一定风险导致卡死)在Task作用域下,使用
try-catch捕获异常后通过UnityDebug输出日志。注册全局异常处理,捕获异步任务中的漏网之鱼,守护最后一道防线;
注意:异常触发可能存在延迟(直到 Task 被垃圾回收时才会检测到未观察的异常)。
TaskScheduler.UnobservedTaskException += (s, e) =>
{
Debug.LogError("Unobserved Task Exception: " + e.Exception);
e.SetObserved(); // 标记异常已处理,避免程序崩溃
};数据多线程同步协作
TouchSocket所有数据采用的是Core的内存池,其数据ByteBlock秉承着谁创建谁销毁原则,任何非用户创建的ByteBlock,都会由创建的线程最后释放。
但是在unity实际过程中,消息来源和unity主线程并不是同一个线程,经常需要把数据提取到主线程下(异步多线程操作),就会触发如下异常:
ObjectDisposedException: Cannot access a disposed object.
Object name: 'The object instance with type 'TouchSocket.Core.ByteBlock' and HashCode '-1015711552' has been released.'.原因非常简单,byteBlock对象在到达HandleReceivedData时,触发Task异步,此时触发线程会立即返回,并释放byteBlock,而Task异步线程会滞后,然后试图从已释放的byteBlock中获取数据,所以,必定发生异常。
解决方案有两种:
官方支持的是:只需要在异步前锁定,然后使用完成后取消锁定,且不用再调用Dispose进行再次释放;这也是最简单的
public class MyTClient : TcpClient
{
protected override bool HandleReceivedData(ByteBlock byteBlock, IRequestInfo requestInfo)
{
byteBlock.SetHolding(true);//异步前锁定
Task.Run(()=>
{
string mes = byteBlock.Span.ToString(Encoding.UTF8);
byteBlock.SetHolding(false);//使用完成后取消锁定,且不用再调用Dispose
Console.WriteLine($"已接收到信息:{mes}");
});
return true;
}
}在插件内(就是在消息线程作用范围内,转换完成数据,只把处理后的数据发出去,这样也符合TouchSocket插件系统的一种方式,可以标记本数据处理完成。
UDP在Win上的UDP链接被重置10054
这个错误是winsock特有的一个BUG;正常理解下,UDP我们发出去的数据,是不管它到不到达的(这样符合UDP不是面向连接的)。
这个错误一般是使用UDP Socket接受时收到(这里我不讲具体的语言了,不管你用C#、Python还是C,在Windows下都会有类似的问题,只要你调用类似ReceiveFrom函数)。这是上一次Send操作向一个地址发送,但是那个地址没有Socket监听(例如对等体崩溃),那么ICMP控制协议会向我们发送一个Port Unreachable错误,当然这个错误应该包含对方的详细地址等信息,但是Winsock把这个错误转化为Connection Reset,在你下一次调用读操作的时候,引发异常,却没把详细信息给你——例如用C#接受到的对方地址是0.0.0.0。
而这个问题最要命的是,如果你不采取措施,每次调用读操作都会引发该异常!唯一恢复正常的办法就是把Socket关掉,重开。这就非常要命:你要实现一个UDP服务器,把收到的音频发给所有的客户,如果某个客户崩溃了或者网络不好,你的Send不会出问题,但是你Receive的时候却出了问题!好吧,你捕获了异常,重新Receive,还是异常!好吧,你关掉Socket重新建立,但是因为不知道是哪个客户出了问题,所以不能及时把他的地址从发送列表里去除(即使使用心跳检测也要等几秒钟),下次Send还是这样,你就不停地关闭创建Socket,谁受的了?
如下图所示,使用一个UDP服务向多个远端发送消息,只要有一个对面没有启动UDP监听,就会触发如下信息,这个时候当前UDP服务调用发送函数仍能正常发送,但是接收是废了的。

对于这种情况,TouchSocket已经添加了对本错误的处理,只需要在配置变量上调用.UseUdpConnReset(),即可解决本问题。
udpClient.Setup(new TouchSocketConfig()
.UseUdpConnReset()
.SetBindIPHost(new IPHost(IPAddress.Any, 0)));
评论