我有一个使用UdpClient
,TcpClient
,TcpListener
的多线程网络应用程序,并使用例如处理接收到的连接和接收数据。 BeginReceive()
EndReceive()
回调模式。
以UdpClient为例,在这种模式中,我使用的一般工作流程是:
UdpClient.BeginReceive()
UdpClient.EndReceive()
收集数据报。UdpClient.BeginReceive()
以准备接收另一个数据报。 问:由于只有一个UdpClient
对象,并且由于在下一个EndReceive()
之前总是调用BeginReceive()
的模式,是否有必要为这些调用锁定/同步acccess到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();
}
答案 0 :(得分:3)
是否有必要为这些调用锁定/同步对象的UdpClient对象?
不,不完全是,但也许不是你想的原因。
如果在处理完当前数据报之前调用BeginReceiveFrom()
(或只是BeginReceive()
),实际上可以同时调用同一个回调。这实际上是否发生取决于很多事情,包括线程调度,线程池中当前有多少IOCP线程,当然还有一个数据报是否可以接收。
所以你肯定有这样的风险:在你完成当前数据报的处理之前,将会收到一个新的数据报,并且在完成第一个数据报的处理之前就会开始处理。
现在,如果数据报的处理涉及访问某些其他共享数据,那么您肯定需要围绕其他共享数据进行同步,以确保安全访问其他数据。
但就数据报本身而言,网络对象是线程安全的,因为你不会同时使用它来破坏对象...它仍然取决于你,以确保你使用它们以连贯的方式。但特别是UDP协议,这比TCP更容易。
UDP不可靠。它有三个非常重要的保证缺乏:
最后一点在这里特别相关。您的代码已经需要能够无序处理数据报。因此,无论数据报的混乱是由于网络本身还是因为您在完成当前处理之前启动了新的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
- 类型字段中存储引用甚至不会非常方便。
所有使static
做的事情都会改变访问该成员的方式。如果不是static
,则需要指定查找成员的实例引用;如果是static
,则只需指定类型即可。这就是全部。它与线程安全本身没有任何关系。
最后,关于您的两个代码示例,它们在功能上是等效的。无需保护对EndReceive()
和BeginReceive()
的调用,并且您的lock
不包含这些方法的任何其他部分(例如数据报的实际处理),所以它并没有真正完成任何事情(除了可能增加上下文切换的开销)。
在并发场景中,在离开lock
之后但是在调用BeginReceive()
之后,第一个线程可能会被抢占。这可能导致第二个线程被唤醒以处理第二个I / O完成的回调。然后第二个线程将命中lock
并停止,允许第一个线程继续执行并离开lock
。但同步所做的一切都是为了减慢速度。它不会阻止数据报数据本身的任何并发访问,这是(可能)重要的部分。