【问题标题】:Do I need to synchronise TCP/UDP clients in async BeginReceive Callback我是否需要在异步 BeginReceive 回调中同步 TCP/UDP 客户端
【发布时间】:2016-01-11 21:42:47
【问题描述】:

我有一个使用UdpClientTcpClientTcpListener 的多线程网络应用程序,并使用例如处理接收到的连接和接收到的数据。 BeginReceive() EndReceive() 回调模式。

以 UdpClient 为例,在这个模式中,我使用的一般工作流程是:

  1. 致电UdpClient.BeginReceive()
  2. 接收到数据报时执行接收回调。
  3. 致电UdpClient.EndReceive()收集数据报。
  4. 再次调用UdpClient.BeginReceive(),准备接收另一个数据报。
  5. 处理在 (3) 处收到的数据报。
  6. 收到更多数据报时重复 2 - 5

问:由于只有一个UdpClient 对象,并且由于总是在下一个BeginReceive() 之前调用EndReceive() 的模式,是否有必要锁定/同步访问那些调用的UdpClient 对象?

在我看来,其他线程不可能干预此工作流程或使这些调用成为非原子调用。 TcpClient.BeginReceive()TcpListener.BeginAcceptTcpClient() 的模式非常相似。

额外问题:是否需要声明单个UdpClient 对象static(如果需要,static 锁定object)?

注意:我询问是否有必要在例如数据报处理。仅关于此模式和 UdpClient TcpClient TcpListener 对象。


编辑

澄清一下,(忽略异常处理)是这段代码:

private void InitUDP()
{
    udpclient = new UdpClient(new IPEndPoint(IPAddress.Any, Settings.Port));

    udpclient.BeginReceive(new AsyncCallback(receiveCallback), udpclient);
}

private void receiveCallback(IAsyncResult ar)
{
    UdpClient client = (UdpClient)ar.AsyncState;

    IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);

    byte[] datagram = client.EndReceive(ar, ref ep);

    udpclient.BeginReceive(new AsyncCallback(receiveCallback), udpclient);

    processDatagram();
}

实际上与此代码不同或保护性较低:

private void InitUDP()
{
    udpclient = new UdpClient(new IPEndPoint(IPAddress.Any, Settings.Port));

    udpclient.BeginReceive(new AsyncCallback(receiveCallback), udpclient);
}

private void receiveCallback(IAsyncResult ar)
{
    UdpClient client = (UdpClient)ar.AsyncState;

    IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);

    lock(_lock)
    {
        byte[] datagram = client.EndReceive(ar, ref ep);

        udpclient.BeginReceive(new AsyncCallback(receiveCallback), udpclient);
    }

    processDatagram();
}

【问题讨论】:

  • UdpClient 成员函数不是线程安全的,所以如果多个线程访问同一个实例,那么您就有问题了。我不确定这是否是您的问题。
  • 这就是问题所在 - 如果总是在下一个 BeginReceive() 之前调用 EndReceive() ,那么另一个线程是否可以通过这些回调访问同一个实例?我需要锁定这些电话吗?
  • 你是线程的控制者,所以你决定哪个线程访问什么。那么有可能吗?是的,如果回调可以合法地访问实例,例如当它是公共的或回调是同一实例的成员时 - 在某种程度上,如果您可以编写代码来访问回调中的实例并且它编译然后,是的,您可以访问它。但除非我误解了,否则这不是你真正想要回答的问题,因为就像我说的,你决定每个回调/线程做什么,对吧?
  • 这是另一个 SO 答案,展示了如何use BeginReceive and EndReceive。它还谈到了使用任务作为简化代码的一种方式,这也清楚地表明你的代码/回调中没有线程。

标签: c# multithreading sockets tcpclient udpclient


【解决方案1】:

是否有必要为这些调用锁定/同步对 UdpClient 对象的访问?

不,不完全是,但也许不是你想的那样。

如果您在处理完当前数据报之前调用BeginReceiveFrom()(或者只是BeginReceive()),实际上可能同时调用相同的回调。这是否真的发生取决于很多事情,包括线程调度、线程池中当前有多少 IOCP 线程可用,当然还有是否有要接收的数据报。

因此,您肯定有这样的风险,即在处理完当前数据报之前,会收到新的数据报,并且会在第一个数据报的处理完成之前开始处理。

现在,如果数据报的处理涉及访问一些其他共享数据,那么您肯定需要围绕其他共享数据进行同步,以确保安全访问其他数据。

但就数据报本身而言,网络对象是线程安全的,因为您不会同时使用它来破坏对象……您仍然需要确保以一致的方式使用它们。但特别是 UDP 协议,这比 TCP 更容易。

UDP 不可靠。它缺乏三个非常重要的保证:

  1. 无法保证数据报一定会送达。
  2. 不能保证一个数据报不会被多次传送。
  3. 不能保证数据报将按照与发送它的其他数据报相同的顺序进行传递。

最后一点在这里特别重要。您的代码已经需要能够处理乱序的数据报。因此,无论是由于网络本身还是因为您在处理完当前的 I/O 操作之前开始了新的 I/O 操作而发生这种数据报的混乱,如果编写正确,您的代码将成功地处理它。


使用 TCP,情况就不同了。您再次遇到同样的问题,如果您已启动 I/O 操作,它肯定会在您处理完当前 I/O 操作之前完成。但与 UDP 不同的是,TCP 确实有一些保证,包括在套接字上接收到的数据将按照发送的顺序接收。

只要您在处理完当前完成的 I/O 操作之前不调用BeginReceive(),一切都很好。您的代码以正确的顺序查看数据。但是,如果您之前调用了BeginReceive(),那么您的当前线程可能会在处理完当前 I/O 操作之前被抢占,而另一个线程可能会结束处理新完成的 I/O 操作。

除非您对接收到的数据进行了某种同步或排序,以解决可能会出现乱序处理 I/O 完成的情况,否则这会损坏您的数据。不好。

同时发出多个接收操作是有充分理由的。但它们通常与对高度可扩展服务器的需求有关。发出多个并发接收操作也有负面影响,包括确保以正确顺序处理数据的额外复杂性,以及在堆中拥有多个固定/固定缓冲区的开销(尽管可以通过多种方式,例如分配足够大的缓冲区以确保它们位于大对象堆中)。

我会避免以这种方式实现代码,除非您有必须解决的特定性能问题。即使在处理 UDP 时,尤其是在处理 TCP 时。如果您确实以这种方式实现代码,请非常小心。

是否需要将单个 UdpClient 对象声明为静态(如果需要,还需要声明静态锁定对象)?

在哪里存储对 UdpClient 对象的引用并不重要。如果您的代码需要一次维护多个UdpClient,将引用存储在单个UdpClient-type 字段中甚至都不是很方便。

static 所做的所有事情都是改变访问该成员的方式。如果不是static,则需要指定该成员所在的实例引用;如果是static,只需要指定类型即可。就这样。它与线程安全本身没有任何关系。


最后,关于您的两个代码示例,它们在功能上是等效的。没有必要保护对EndReceive()BeginReceive() 的调用,并且您的lock 不包含这些方法的任何其他部分(例如数据报的实际处理),因此它并没有真正完成任何事情(其他而不是可能增加上下文切换的开销)。

在并发场景中,第一个线程可能会在离开lock 之前但在调用BeginReceive() 之后被抢占。这可能会导致第二个线程被唤醒以处理第二个 I/O 完成的回调。然后第二个线程将命中lock 并停止,允许第一个线程继续执行并离开lock。但是同步所做的只是减慢速度。它不会阻止数据报数据本身的任何并发访问,这是(可能)重要的部分。

【讨论】:

  • 谢谢@Peter,非常全面的回答和讨论。这让我受益匪浅。如果我正确理解您的观点,除非我的性能要求很高,否则对于所有协议,您似乎建议在 EndReceive() 调用之后和随后的 BeginReceive() 调用之前移动 processDatagram()。对于我的特定应用程序,这会很好。至于数据报处理本身,我目前正在排队和锁定队列,这个在别处处理。
  • 澄清:至于数据报处理本身,我目前正在排队这些数据,并锁定队列以进行读写访问,这在其他地方处理。数据本身是共享的和线程安全的。
  • 是的,如果您没有具体的、观察到的、可量化的性能问题要解决,我会确保在您下次调用 BeginReceive() 之前执行您对 processDatagram() 的调用。这将使事情变得简单。
  • @MicroVirus:“异步模式不会调用不同线程上的处理程序”——你肯定不正确。在整个 .NET 中,异步模式几乎总是使用不同的线程来完成。明显的例外是Control.BeginInvoke()Dispatcher.BeginInvoke()(它们专门设计用于在特定线程上工作)。特别是网络 I/O 由 IOCP 线程池处理,任意线程用于处理 I/O 完成。
  • @MicroVirus:因为使用了线程池,如果负载低,您可能会发现池中只有一个线程,这会使您看起来总是在同一个线程上完成。但实际上,那里发生的一切只是没有足够的活动来让线程池创建新线程。在负载下,它会,并且那些额外的线程用于在 I/O 完成时处理它们,而不考虑 I/O 的启动位置。
猜你喜欢
  • 2010-10-15
  • 2019-09-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多