我是否需要在异步 BeginReceive 回调中同步 TCP/UDP 个客户端

Do I need to synchronise TCP/UDP clients in async BeginReceive Callback

我有一个使用 UdpClientTcpClientTcpListener 的多线程网络应用程序,并使用例如BeginReceive() EndReceive() 回调模式。

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

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

问:由于只有一个UdpClient对象,并且由于总是在下一个[=16=之前调用EndReceive()的模式],是否需要 lock/synchronise 访问这些调用的 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();
}

is it necessary to lock/synchronise acccess to the UdpClient object for those calls?

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

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

所以你肯定有这样的风险,在你完成当前数据报的处理之前,将收到一个新的数据报,并且在第一个数据报处理完成之前开始处理它。

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

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

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

  1. 不能保证一定会传送数据报。
  2. 不能保证数据报不会被传递超过一次。
  3. 不能保证数据报将以与发送它的其他数据报相同的顺序传送。

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


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

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

除非您对接收到的数据进行了某种同步或排序以解决 I/O 无序处理完成的可能性,否则这将破坏您的数据。不好。

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

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

Does the single UdpClient object need to be declared static (and static lock object if one is required)?

在哪里存储对 UdpClient 对象的引用并不重要。如果您的代码需要同时维护多个 UdpClient,那么将引用存储在单个 UdpClient 类型的字段中甚至不会很方便。

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


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

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