我有一个使用UdpClient
,TcpClient
,TcpListener
的多线程网络应用程序,并使用例如处理接收的连接和接收的数据。 BeginReceive()
EndReceive()
回调模式。
以UdpClient为例,在这种模式下,我正在使用的一般工作流程是:
UdpClient.BeginReceive()
UdpClient.EndReceive()
收集数据报。 UdpClient.BeginReceive()
以准备接收另一个数据报。 问:由于仅存在一个
UdpClient
对象,并且由于始终在下一个EndReceive()
之前调用BeginReceive()
的模式,是否有必要将这些调用的访问权限锁定/同步到UdpClient
对象?在我看来,另一个线程不可能干预此工作流程或使这些调用成为非原子调用。
TcpClient.BeginReceive()
和TcpListener.BeginAcceptTcpClient()
的模式非常相似。奖励Q:是否需要将单个
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();
}
最佳答案
不,不完全是,但可能不是出于您可能会想的原因。
如果在处理完当前数据报之前调用BeginReceiveFrom()
(或者只是BeginReceive()
),则实际上有可能同时调用同一回调。是否真的会发生这种情况取决于很多事情,包括线程调度,线程池中当前有多少个IOCP线程可用,当然,甚至还有一个数据报要接收。
因此,您绝对有风险,在完成当前数据报的处理之前,将发生新数据报的接收,并且将在处理第一个数据报之前开始对其进行处理。
现在,如果数据报的处理涉及对某些其他共享数据的访问,那么您确实确实需要围绕该其他共享数据进行同步,以确保对其他数据的安全访问。
但是就数据报本身而言,网络对象是线程安全的,从某种意义上讲,您不会同时使用该对象来破坏该对象……仍然需要确保您以一致的方式使用它们。但是特别是对于UDP协议(protocol),这比TCP更容易。
UDP不可靠。它缺乏三个非常重要的保证:
最后一点在这里特别重要。您的代码已经需要能够无序地处理数据报。因此,不管这种数据报困惑是由于网络本身发生,还是由于您在处理完当前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
-type字段中甚至都不是很方便。制作
static
所做的所有事情就是改变了该成员的访问方式。如果不是static
,则需要指定在其中找到该成员的实例引用;如果是static
,则只需指定类型。就这样。它与线程安全本身根本没有任何关系。最后,对于您的两个代码示例,它们在功能上是等效的。无需保护对
EndReceive()
和BeginReceive()
的调用,并且您的lock
不包含这些方法的任何其他部分(例如,数据报的实际处理),因此它实际上并没有完成任何事情(除了可能会增加开销)上下文切换)。在并发场景中,有可能在离开
lock
之前但在调用BeginReceive()
之后会抢占第一个线程。这可能会导致第二个线程被唤醒,以处理用于第二次I/O完成的回调。然后,第二个线程将命中lock
并停顿,从而允许第一个线程恢复执行并离开lock
。但是同步所做的只是使速度变慢。它不会阻止对数据报数据本身的任何并发访问,这是(可能)重要的部分。关于c# - 我是否需要在异步BeginReceive回调中同步TCP/UDP客户端,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/33114541/