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

时间:2015-05-13 18:07:06

标签: c# .net asynchronous memory-leaks idisposable

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

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

例如,如果我在UdpClient.BeginReceive中使用UdpListener

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.EndReceive上调用_udpClient(这只会产生ObjectDisposedException)?

修改

在完成所有异步操作之前配置UdpClient(和其他IDisposable s)作为取消或实现超时的方法并不罕见,特别是在operations that will never complete之上。这也是建议的throughout this site

2 个答案:

答案 0 :(得分:4)

  

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

     

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

这与实现IDisposable的类无关。

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

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

  1. 在部署EndXXX实例后调用UDPClient 会导致它直接抛出ObjectDisposedException
  2. EndXXX电话中没有处理任何资源。
  3. 与此操作相关的资源(本机重叠和固定非托管缓冲区)将在异步操作完成回调中被回收。
  4. 所以在这种情况下,它是完全安全的,没有泄漏。

    作为一般方法

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

    1. 实施可能在未来发生变化,打破您的假设。
    2. 有更好的方法可以执行此操作,使用取消和超时进行异步(I / O)操作(例如,通过调用Close实例上的_udpClient来强制I / O失败)。
    3. 另外,我不想依赖于我检查整个调用堆栈(并没有犯这样做的错误)以确保不会泄漏任何资源。

      推荐和记录的方法

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

        

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

      基础Socket.BeginReceive方法的以下内容:

        

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

           

      要取消待审BeginReceive,请调用Close方法。

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

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

      public class UdpListener : IDisposable
      {
          private readonly IPAddress _hostIpAddress;
          private readonly int _port;
          private readonly Action<UdpReceiveResult> _processor;
          private TaskCompletionSource<bool> _tcs = new TaskCompletionSource<bool>();
          private CancellationTokenSource _tokenSource = new CancellationTokenSource();
          private CancellationTokenRegistration _tokenReg;
          private UdpClient _udpClient;
      
          public UdpListener(IPAddress hostIpAddress, int port, Action<UdpReceiveResult> 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, i.e. completed normally
                  _tcs.SetResult(true);
              }
              catch (Exception ex)
              {
                  // we failed.
                  _tcs.TrySetException(ex); 
              }
          }
      }
      

答案 1 :(得分:0)

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

这种行为当然取决于具体的实施方式,但它应该是安全的,EndXXXUdpClientTcpClient等等确实如此......

由于APM早于TPL和随附的Socket,因此您通常无法使用CancelationToken取消这些异步操作。这就是为什么你也无法通过等效的CancelationToken方法(例如UdpClient.RecieveAsync)传递CancelationToken,因为它们只是wrapper over the BeginXXX/EndXXX methods with a call to Task.Factory.FromAsync。此外,超时(如Socket.ReceiveTimeout)通常只会影响同步选项,而不会影响异步选项。

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

根据我们对APM的了解和ObjectDisposedException来电IDisposable应该足以清除任何悬挂的资源并添加对Dispose的调用只会引起无用的EndXXX仅此而已。调用ObjectDisposedException可能会保护您开发人员不遵守指南的地方(可能不会,这取决于错误的实施)但是如果不是全部的话,那么调用它将是安全的.Net&#39;实施,其余部分应该是安全的。

  1. "Consider providing method Close(), in addition to the Dispose(), if close is standard terminology in the area. When doing so, it is important that you make the Close implementation identical to Dispose and consider implementing the IDisposable.Dispose method explicitly"

  2. "Do throw an ObjectDisposedException from any member that cannot be used after the object has been disposed of".

  3. "To cancel a pending call to the BeginConnect method, close the Socket. When the Close method is called while an asynchronous operation is in progress, the callback provided to the BeginConnect method is called."