异步/等待,TAP和EAP

时间:2013-12-17 11:06:44

标签: c# async-await tcpclient

我正在尝试从异常套接字代码中移除(BeginSend / EndSendBeginReceive / 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(“运行缓冲区”)并将消息拆分为消息“部分”。对于每个“部分”,都会引发一个事件。该类的许多实例都可以在系统中运行。

我的问题是:

  1. 我是否正确地假设/理解MyInternalStringBuilder.Append从未被称为“乱序”?每次TcpListener收到数据时,都会将“按顺序”添加到(内部)字符串构建器?我不需要使用锁?
  2. 由于这个StartReceive方法使用内部(“无限”)循环并引发事件,我没有看到制作StartReceive方法async的重点,但我必须(显然能够完全使用await。我知道我正在混合使用TAP / EAP,但我必须提出与此问题无关的原因。但是,感觉“脏”,因为“async不应该void”是我到目前为止收集的内容。也许有更好的解决方法(除了全部转移到TAP)?

4 个答案:

答案 0 :(得分:3)

是的,它将是有序的。因为您只有一个活动缓冲区*,所以您在发布下一个活动缓冲区之前处理它,事件顺序是确定性的(post-> receive-> process-> post-> receive ...)。如果您在此循环中仅使用SB,则无需锁定。

然而,对于MessageReceived事件中发生的事情,显然有很大的'if'。假设你做了体面的事情(例如,流程和发布回复),应该没问题。如果你试图从事件中获得更多东西,那么一切都会破裂(例如,发送一个响应,然后等待对响应的响应,这将是不好的)。如果您的处理是事件驱动的状态机('状态'栏中收到消息'foo',用'垃圾'回复并将状态更改为'bam',返回循环等待更多事件),那么它通常应该没问题。很明显,很难给出没有代码的判决,所有基础都是你的主张,也不是通过提出这些主张来理解你所理解的内容(没关系,你似乎知道你在说什么)。

您描述的处理速度不是最快的(因为当您处理当前缓冲区时,传入的字节没有缓冲空间)但实现高吞吐量会更加棘手,正是因为您提示的订单问题。此外,如果流量是请求 - 响应,那么它确实无关紧要。

posted buffer:向网络提交了一个缓冲区以填充它。在.Net中,流操作和AFD / tcp.sys之间有大约1百万个抽象层,但这个概念几乎相同。

答案 1 :(得分:2)

完成Remus的回答,您可能希望确保StartReceive只能被调用一次,否则您将遇到严重问题并需要锁定。

关于void返回,我个人认为这是一个很好的案例,从StartBegin开始的“顶级”异步方法,正确地表达了这是一种即发即弃的方法。也就是说,如果你有这样的方法,你可能想重新设计。

为此,我将使用诸如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关键字的情况下编写整个事物,而是在任务或线程中调用它?