如果实例已经处理掉,在不调用EndXXX的情况下调用BeginXXX是否安全

使用异步编程模型时 ,通常建议将每个BeginXXXEndXXX匹配,否则在异步操作完成之前可能会泄漏资源。

如果类实现了IDisposable并且通过调用Dispose实例,情况仍然如此吗?

例如,我在UdpListener使用UdpClient.BeginReceive

 class UdpListener : IDisposable { private bool _isDisposed; private readonly IPAddress _hostIpAddress; private readonly int _port; private UdpClient _udpClient; public UdpListener(IPAddress hostIpAddress, int port) { _hostIpAddress = hostIpAddress; _port = port; } public void Start() { _udpClient.Connect(_hostIpAddress, _port); _udpClient.BeginReceive(HandleMessage, null); } public void Dispose() { if (_isDisposed) { throw new ObjectDisposedException("UdpListener"); } ((IDisposable) _udpClient).Dispose(); _isDisposed = true; } private static void HandleMessage(IAsyncResult asyncResult) { // handle... } } 

我是否还需要确保在处置的_udpClient上调用UdpClient.EndReceive (这只会导致ObjectDisposedException )?


编辑:

在完成所有异步操作之前,将UdpClient (和其他IDisposable s)配置为取消或实现超时的方式并不罕见,尤其是在永远不会完成的操作上 。 这也是本 网站推荐的内容。

使用异步编程模型时,通常建议将每个BeginXXXEndXXX进行匹配,否则在异步操作仍处于“运行”状态时可能存在泄漏资源的风险。

如果类实现了IDisposable并且在实例上调用了Dispose ,那么仍然如此吗?

这与实现IDisposable的类无关。

除非您可以确定异步完成将释放与通过BeginXXX启动的异步操作相关的任何资源,并且不执行清除,或者由于EndXXX调用,您需要确保匹配您的呼叫。 确定这一点的唯一方法是检查特定异步操作的实现。

对于您选择的UdpClient示例,恰好是这样的情况:

  1. 处理UDPClient实例调用EndXXX将导致它直接抛出ObjectDisposedException
  2. EndXXX呼叫中没有配置资源或者没有配置资源。
  3. 与此操作相关的资源(本机重叠和固定的非托管缓冲区)将在异步操作完成回调中回收。

所以在这种情况下它是完全安全的,没有泄漏。

作为一般方法

我不相信这种方法作为一般方法是正确的,因为:

  1. 实施可能在未来发生变化,打破您的假设。
  2. 有更好的方法可以使用异步(I / O)操作的取消和超时(例如,通过在_udpClient实例上调用Close来强制I / O失败)。

此外,我不想依赖我检查整个调用堆栈(并没有犯这样做的错误),以确保不会泄漏任何资源。

推荐和记录的方法

请从UdpClient.BeginReceive方法的文档中注意以下内容:

必须通过调用EndReceive方法来完成异步BeginReceive操作。 通常,requestCallback委托调用该方法。

以下是底层的Socket.BeginReceive方法:

必须通过调用EndReceive方法来完成异步BeginReceive操作。 通常,该方法由回调委托调用。

要取消挂起的BeginReceive ,请调用Close方法。

即这是“按设计”记录的行为。 你可以争论设计是否非常好,但很明显取消的预期方法是什么,以及你可以期待的行为。

对于您的特定示例(更新为对异步结果执行有用的操作)以及与其类似的其他情况,以下是遵循建议方法的实现:

 public class UdpListener : IDisposable { private readonly IPAddress _hostIpAddress; private readonly int _port; private readonly Action _processor; private TaskCompletionSource _tcs = new TaskCompletionSource(); private CancellationTokenSource _tokenSource = new CancellationTokenSource(); private CancellationTokenRegistration _tokenReg; private UdpClient _udpClient; public UdpListener(IPAddress hostIpAddress, int port, Action processor) { _hostIpAddress = hostIpAddress; _port = port; _processor = processor; } public Task ReceiveAsync() { // note: there is a race condition here in case of concurrent calls if (_tokenSource != null && _udpClient == null) { try { _udpClient = new UdpClient(); _udpClient.Connect(_hostIpAddress, _port); _tokenReg = _tokenSource.Token.Register(() => _udpClient.Close()); BeginReceive(); } catch (Exception ex) { _tcs.SetException(ex); throw; } } return _tcs.Task; } public void Stop() { var cts = Interlocked.Exchange(ref _tokenSource, null); if (cts != null) { cts.Cancel(); if (_tcs != null && _udpClient != null) _tcs.Task.Wait(); _tokenReg.Dispose(); cts.Dispose(); } } public void Dispose() { Stop(); if (_udpClient != null) { ((IDisposable)_udpClient).Dispose(); _udpClient = null; } GC.SuppressFinalize(this); } private void BeginReceive() { var iar = _udpClient.BeginReceive(HandleMessage, null); if (iar.CompletedSynchronously) HandleMessage(iar); // if "always" completed sync => stack overflow } private void HandleMessage(IAsyncResult iar) { try { IPEndPoint remoteEP = null; Byte[] buffer = _udpClient.EndReceive(iar, ref remoteEP); _processor(new UdpReceiveResult(buffer, remoteEP)); BeginReceive(); // do the next one } catch (ObjectDisposedException) { // we were canceled, ie completed normally _tcs.SetResult(true); } catch (Exception ex) { // we failed. _tcs.TrySetException(ex); } } } 

考虑到事实, Dispose (应该与Close 1相同)释放任何非托管资源(GC释放托管的资源),并且当在已处置的实例2上调用时,方法抛出ObjectDisposedException ,不应该调用EndXXX 安全的。

当然这种行为取决于具体的实现,但它应该是安全的,在UdpClientTcpClientSocket等中确实如此……

由于APM早于TPL和随附的CancelationToken ,因此通常无法使用CancelationToken来取消这些异步操作。 这就是为什么你也不能在等效的async-await方法(例如UdpClient.RecieveAsync )上传递Task.Factory.FromAsync因为它们只是对BeginXXX / EndXXX方法的一个包装器,调用了Task.Factory.FromAsync 。 此外,超时(如Socket.ReceiveTimeout )通常只影响同步选项而不影响异步选项。

取消此类操作的唯一方法是通过释放实例本身3来释放所有资源并调用所有等待的回调,这些回调通常会调用EndXXX并获取相应的ObjectDisposedException 。 在处理实例时,通常会从这些方法的第一行引发此exception。

根据我们对APM和IDisposable调用的了解, Dispose应该足以清除任何挂起的资源,并且添加对EndXXX的调用只会引发无用的ObjectDisposedException ,仅此而已。 调用EndXXX可能会在开发人员没有遵循指导原则的情况下保护您(可能不会,这取决于错误的实现),但是如果不是全部.Net的实现,那么调用它将是安全的,并且在其余部分应该是安全的。


  1. “除了Dispose()之外,考虑提供方法Close() ,如果close是区域中的标准术语。这样做时,将Close实现 Dispose 相同并考虑明确地实现IDisposable.Dispose方法是很重要的。 “

  2. “从处理对象后无法使用的任何成员抛出ObjectDisposedException

  3. “要取消对BeginConnect方法的挂起调用,请关闭Socket 。当异步操作正在进行时调用Close方法时,将调用提供给BeginConnect方法的回调。”