如果实例已经被释放,在不调用 EndXXX 的情况下调用 BeginXXX 是否安全 [英] Is it safe to call BeginXXX without calling EndXXX if the instance is already disposed

查看:15
本文介绍了如果实例已经被释放,在不调用 EndXXX 的情况下调用 BeginXXX 是否安全的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

When using the Asynchronous Programming Model it is usually recommended to match every BeginXXX with an EndXXX, otherwise you risk leaking resources until the asynchronous operation completes.

如果类实现了 IDisposable 并且实例是通过调用 Dispose 释放的,那么情况仍然如此吗?

Is that still the case if the class implements IDisposable and the instance was disposed by calling Dispose?

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

If for example I use UdpClient.BeginReceive in a 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 上调用 UdpClient.EndReceive(这只会导致 ObjectDisposedException)?

Do I still need to make sure UdpClient.EndReceive is called on the disposed _udpClient (which will just result in an ObjectDisposedException)?

在所有异步操作完成之前处理UdpClient(和其他IDisposable)作为取消或实现超时的一种方式并不少见,尤其是在永远不会完成的操作.这也是推荐的整个这个 网站.

It isn't uncommon to dispose a UdpClient (and other IDisposables) before all asynchronous operations completed as a way to cancel or implement a timeout, especially over operations that will never complete. This is also what's recommended throughout this site.

推荐答案

当使用异步编程模型时,通常建议将每个 BeginXXX 与一个 EndXXX 匹配,否则您可能会在异步操作仍在运行"时泄漏保留的资源.

When using the Asynchronous Programming Model it is usually recommended to match every BeginXXX with an EndXXX, otherwise you risk leaking resources kept while the asynchronous operation is still "running".

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

Is that still the case if the class implements IDisposable and Dispose was called on the instance?

这与是否实现 IDisposable 的类无关.

This has nothing to do with a class implementing IDisposable or not.

除非您可以确定异步完成会释放与通过 BeginXXX 启动的异步操作相关的任何资源,并且不会在 中或作为结果执行任何清理EndXXX 调用,您需要确保匹配您的调用.唯一确定的方法是检查特定异步操作的实现.

Unless you can be sure that the async completion will free up any resources tied up with the async operation initiated through BeginXXX, and no cleanup is performed in, or as a result of the EndXXX call, you need to ensure that you match your calls. The only way to be certain of this, is to inspect the implementation of a specific async operation.

对于 UdpClient 你选择的例子,恰好是这样的:

For the UdpClient example you chose, it happens to be the case that:

  1. 调用 EndXXX after 处理 UDPClient 实例将导致它直接抛出 ObjectDisposedException.
  2. 没有资源在 EndXXX 调用中或作为其结果处理.
  3. 与此操作相关的资源(本机重叠和固定的非托管缓冲区)将在异步操作完成回调中回收.
  1. Calling EndXXX after disposing the UDPClient instance will result in it directly throwing an ObjectDisposedException.
  2. No resources are disposed in or as a result of the EndXXX call.
  3. The resources tied up with this operation (native overlapped and pinned unmanaged buffers), will be recycled on the async operation completion callback.

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

So in this case it is perfectly safe, without leakage.

作为一般方法

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

I don't believe this approach is correct as a general approach, because:

  1. 实施可能会在未来发生变化,从而打破您的假设.
  2. 有更好的方法来做到这一点,对异步 (I/O) 操作使用取消和超时(例如,通过在 _udpClient 实例上调用 Close强制 I/O 失败).
  1. The implementation could change in the future, breaking your assumptions.
  2. There are better ways to do this, using cancellation and time-outs for your async (I/O) operations (e.g. by calling Close on the _udpClient instance to force an I/O failure).

此外,我不想依靠我检查整个调用堆栈(并且不会犯错误)来确保不会泄漏任何资源.

Also, I would not want to rely on me inspecting the entire call stack (and not making a mistake in doing so) to ensure that no resources will be leaked.

推荐和记录的方法

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

Please note the following from the documentation for the UdpClient.BeginReceive method:

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

The asynchronous BeginReceive operation must be completed by calling the EndReceive method. Typically, the method is invoked by the requestCallback delegate.

以下是基础Socket.BeginReceive 方法:

And the following for the underlying Socket.BeginReceive method:

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

The asynchronous BeginReceive operation must be completed by calling the EndReceive method. Typically, the method is invoked by the callback delegate.

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

To cancel a pending BeginReceive, call the Close method.

即这是按设计"记录的行为.您可以争论设计是否非常好,但很清楚预期的取消方法是什么,以及您可以预期这样做的结果的行为.

I.e. this is the "by design" documented behavior. You can argue whether the design is very good, but it is clear in what the expected approach to cancellation is, and the behavior that you can expect as the result of doing so.

对于您的特定示例(已更新以对异步结果做一些有用的事情)以及其他类似的情况,以下将是遵循推荐方法的实现:

For your specific example (updated to do something useful with the async result), and other situations similar to it, the following would be an implementation that follows the recommended approach:

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); 
        }
    }
}

这篇关于如果实例已经被释放,在不调用 EndXXX 的情况下调用 BeginXXX 是否安全的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆