我正在尝试从异常套接字代码中移除(BeginSend
/ EndSend
,BeginReceive
/ EndReceive
以及许多内部“管理”到异步TcpClient。我对async / await很新,所以请耐心等待......
假设以下代码(无关代码被剥离):
public async void StartReceive()
{
while (true)
{
var stream = this.MyInternalTcpClient.GetStream();
if (stream == null) return;
var buffer = new byte[BUFFERSIZE];
var bytesread = await stream.ReadAsync(buffer, 0, BUFFERSIZE);
if (bytesread == 0)
{
if (Closed != null)
Closed(this, new ClosedEventArgs());
return;
}
var message = this.Encoding.GetString(buffer, 0, bytesread);
this.MyInternalStringBuilder.Append(message);
// ... message processing here ...
foreach (var p in parts) {
//Raise event per message-"part"
if (MessageReceived != null)
MessageReceived(this, new MessageReceivedEventArgs(p));
}
}
}
我的类有一个内部字符串构建器,每次收到数据时都会附加(这是因为消息可以在多个接收'事件'中拆分)。然后,当满足某些条件时,处理stringbuilder(“运行缓冲区”)并将消息拆分为消息“部分”。对于每个“部分”,都会引发一个事件。该类的许多实例都可以在系统中运行。
我的问题是:
MyInternalStringBuilder.Append
从未被称为“乱序”?每次TcpListener
收到数据时,都会将“按顺序”添加到(内部)字符串构建器?我不需要使用锁?StartReceive
方法使用内部(“无限”)循环并引发事件,我没有看到制作StartReceive方法async
的重点,但我必须(显然能够完全使用await
。我知道我正在混合使用TAP / EAP,但我必须提出与此问题无关的原因。但是,感觉“脏”,因为“async
不应该void
”是我到目前为止收集的内容。也许有更好的解决方法(除了全部转移到TAP)?答案 0 :(得分:3)
是的,它将是有序的。因为您只有一个活动缓冲区*,所以您在发布下一个活动缓冲区之前处理它,事件顺序是确定性的(post-> receive-> process-> post-> receive ...)。如果您在此循环中仅使用SB,则无需锁定。
然而,对于MessageReceived
事件中发生的事情,显然有很大的'if'。假设你做了体面的事情(例如,流程和发布回复),应该没问题。如果你试图从事件中获得更多东西,那么一切都会破裂(例如,发送一个响应,然后等待对响应的响应,这将是不好的)。如果您的处理是事件驱动的状态机('状态'栏中收到消息'foo',用'垃圾'回复并将状态更改为'bam',返回循环等待更多事件),那么它通常应该没问题。很明显,很难给出没有代码的判决,所有基础都是你的主张,也不是通过提出这些主张来理解你所理解的内容(没关系,你似乎知道你在说什么)。
您描述的处理速度不是最快的(因为当您处理当前缓冲区时,传入的字节没有缓冲空间)但实现高吞吐量会更加棘手,正是因为您提示的订单问题。此外,如果流量是请求 - 响应,那么它确实无关紧要。
posted buffer
:向网络提交了一个缓冲区以填充它。在.Net中,流操作和AFD / tcp.sys之间有大约1百万个抽象层,但这个概念几乎相同。
答案 1 :(得分:2)
完成Remus的回答,您可能希望确保StartReceive
只能被调用一次,否则您将遇到严重问题并需要锁定。
关于void
返回,我个人认为这是一个很好的案例,从Start
或Begin
开始的“顶级”异步方法,正确地表达了这是一种即发即弃的方法。也就是说,如果你有这样的方法,你可能想重新设计。
为此,我将使用诸如TPL Dataflow之类的库,其中不同的动作表示为异步块。在您的情况下,将有一个“从套接字块读取”→“过程块”→“发送响应块”,每个块都是异步触发另一个,允许您在处理时继续读取。通过这样做,您将不会有这个大循环,并且void
返回将不再存在。但是很多事情都要改变。
答案 2 :(得分:2)
我是否正确地假设/理解MyInternalStringBuilder.Append永远不会被称为“乱序”?每次TcpListener接收数据时,它都会“按顺序”添加到(内部)stringbuilder?我不需要使用锁?
这是正确的。 async
代码虽然是异步的,但自然是顺序的。因此,即使封底StartReceive
不断返回并恢复,它也不会同时恢复多次。
由于这个StartReceive方法使用内部(“无限”)循环并引发事件,我没有看到使StartReceive方法异步的观点,但我必须(显然能够使用await)。我知道我正在混合使用TAP / EAP,但我必须提出与此问题无关的原因。然而,感觉“脏”,因为“异步不应该是无效的”是我到目前为止所收集的。也许有更好的方法来解决这个问题(除了全部转移到TAP)?
我不是async void
的忠实粉丝。首先,假设您在try
内有一个顶级catch
/ StartReceive
,如果发现任何异常,将启动套接字关闭(如果没有,则需要一个)。就个人而言,我会将其作为async Task
方法编写,并考虑将Task
公开为属性(如果您还为错误处理设置了所有事件,那么您不需要{{1}属性)。
还有另外一件需要注意的事项;套接字新手经常写的严格只读/只写循环存在问题:在只读部分,读者无法检测半开场景;在只写部分期间,存在(微小的)死锁机会,特别是对于恶意客户端。理想情况下,套接字应始终具有读取操作(您执行此操作),并且还应定期发送某些内容(例如,keepalive)。我有一个更详细的sockets FAQ。
要解决此问题,建议您将Task
添加到所有ConfigureAwait(false)
,并将您的EAP事件发布到构建器中捕获的await
(使用SynchronizationContext
没有一个)。然后你需要另一个独立的循环,它可以定期发送keepalive消息(如果协议允许的话),或者如果它在一段时间内没有读取任何内容就终止套接字。
答案 3 :(得分:1)
如果你在多个线程之间共享StringBuilder,你肯定需要使用锁。
另外,我不理解按顺序的含义。将在 订单中调用Append
。它可能不是您想要的顺序,但是此代码中没有任何内容可以确保任何特定的顺序。
我可以说在内部调用<{1}} 的方法将按照代码执行它们的顺序调用,这应该与您对代码的期望完全一样,但同样,如果多个线程同时调用它,则不会给出线程之间的顺序。
至于你的第二个问题,为什么不简单地在不使用Append
关键字的情况下编写整个事物,而是在任务或线程中调用它?