如果实例已被释放,是否安全调用BeginXXX而不调用EndXXX?

4
当使用异步编程模型时,通常建议将每个BeginXXX与一个EndXXX匹配,否则在异步操作完成之前,您可能会泄漏资源。
如果类实现了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),特别是在 永远不会完成的操作 上。这也是建议的做法在此 网站上


7
你可能在操作完成之前就放弃了你的客户端。你几乎肯定不希望这样做。该代码存在“正确性”问题而非性能问题。 - Servy
1
Servy 的意思是,您不希望在使用语句中使用 APM,在该语句中,在 APM 调用甚至开始之前就会退出块。问题是在短暂的 using 中使用 APM,在您的情况下,客户端对象需要与整个 Begin/End 具有相同的生命周期,而不仅仅是 Begin。 - Ron Beyer
EndOperationName 方法的目的不仅是释放资源,还要确保 Async 方法完成。如果在调用 EndOperationName 时,由 IAsyncResult 对象表示的异步操作尚未完成,则 EndOperationName 会阻塞调用线程,直到异步操作完成。这是您提供的链接中的摘录。您是否真正完整地阅读了它? - Der Kommissar
如果您在已释放的 _udpClient 实例上调用 EndReceive,它将仅抛出一个 ObjectDisposedException 而不做任何其他操作。因此,不,您不应该这样做,尽管我很难想象在这种情况下何时/何地会调用 EndReceive - Alex
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Der Kommissar
显示剩余17条评论
2个回答

4
在使用异步编程模型时,通常建议每个BeginXXXEndXXX相匹配,否则您可能会泄漏在异步操作仍在“运行”时保留的资源。如果类实现了IDisposable并且在实例上调用了Dispose,那么这种情况是否仍然适用?
这与类是否实现IDisposable无关。除非您可以确信异步完成将释放通过BeginXXX启动的异步操作所绑定的任何资源,并且在EndXXX调用中没有执行任何清理或作为其结果进行清理,否则需要确保匹配调用。唯一确定此事的方法是检查特定异步操作的实现。
对于您选择的{{link1:UdpClient}}示例,恰好是这种情况:
在释放UDPClient实例后调用EndXXX会直接导致它抛出一个ObjectDisposedException异常。 EndXXX调用不会释放任何资源。
与此操作相关的资源(本地重叠和固定未管理的缓冲区)将在异步操作完成回调时被回收。
因此,在这种情况下,没有泄漏是完全安全的。
作为一般方法,
我认为这种方法并不正确,因为:
1. 实现可能会在未来发生变化,打破您的假设。
2. 有更好的方法来使用取消和超时来处理您的异步(I/O)操作(例如通过在_udpClient实例上调用Close来强制进行I/O失败)。
此外,我不想依赖于检查整个调用堆栈(并且不犯错误)以确保不会泄漏任何资源。
推荐并记录的方法是:
请注意 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); 
        }
    }
}

如果IDisposable的目的是释放资源,且已释放的对象会在实例方法(如 EndXXX)中抛出 ObjectDisposedException,那么“确信异步完成将释放任何资源”与 IDisposable 无关。 - i3arnon
@i3arnon,您正在依赖于当前代码实现的检查结果,而不是这些类的已发布文档。文档明确说明了如何取消这些操作以及在取消时预期的行为。 - Alex
我认为实现更重要,但无论如何,文档并没有明确说明应该如何取消这些操作(或结果会是什么)。然而,文档隐含了这些操作是不可取消的事实。你唯一能够取消这些操作(例如在UdpClient上异步版本的Receive)的方法是调用实例上的Dispose - i3arnon
@i3arnon:有很多情况下,服务的线程外“Dispose”意味着“开始优雅地关闭自己”。当发生Dispose时正在执行的事务应该被回滚或完成,并且处理不应阻止已完成的事务被报告为已完成。如果将“Dispose”的含义视为“考虑到你的所有者不再对你执行工作感兴趣”,并且将IDisposable或其他资源的所有权视为通知它在你不再对其感兴趣时的义务,那么... - supercat
@supercat,这并不是 IDisposable 的真正含义。它的含义是有未经处理的资源等待 Dispose 释放。APM 等待 EndXXX 释放资源。我的意思是,当你将两者结合起来时,在 Dispose 后你就不需要 调用 EndXXX。你仍然可以调用它,也许会得到一个结果?是的。但极不可能,而且即使这样,因为已经超时,不调用它仍然是安全的。 - i3arnon
显示剩余18条评论

0
考虑到以下事实,Dispose(应该与Close1相同)释放任何未受管控的资源(GC 释放受管控的资源),并且当在已处理的实例上调用方法时,会抛出ObjectDisposedException2,因此不调用EndXXX应该是安全的。
当然,这种行为取决于具体的实现,但它应该是安全的,确实是在UdpClientTcpClientSocket等中的情况。

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

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

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


  1. 如果“关闭”是标准术语,请考虑提供方法Close(),除了Dispose()之外。 在这样做时,重要的是使Close实现与Dispose完全相同,并考虑显式实现IDisposable.Dispose方法。

  2. “对于任何在对象被处理后无法使用的成员,都应该抛出ObjectDisposedException异常。”.

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接