如果实例已经处理掉,在不调用EndXXX的情况下调用BeginXXX是否安全
使用异步编程模型时 ,通常建议将每个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
s)配置为取消或实现超时的方式并不罕见,尤其是在永远不会完成的操作上 。 这也是本 网站推荐的内容。
使用异步编程模型时,通常建议将每个
BeginXXX
与EndXXX
进行匹配,否则在异步操作仍处于“运行”状态时可能存在泄漏资源的风险。如果类实现了
IDisposable
并且在实例上调用了Dispose
,那么仍然如此吗?
这与实现IDisposable
的类无关。
除非您可以确定异步完成将释放与通过BeginXXX
启动的异步操作相关的任何资源,并且不执行清除,或者由于EndXXX
调用,您需要确保匹配您的呼叫。 确定这一点的唯一方法是检查特定异步操作的实现。
对于您选择的UdpClient
示例,恰好是这样的情况:
- 在处理
UDPClient
实例后调用EndXXX
将导致它直接抛出ObjectDisposedException
。 -
EndXXX
呼叫中没有配置资源或者没有配置资源。 - 与此操作相关的资源(本机重叠和固定的非托管缓冲区)将在异步操作完成回调中回收。
所以在这种情况下它是完全安全的,没有泄漏。
作为一般方法
我不相信这种方法作为一般方法是正确的,因为:
- 实施可能在未来发生变化,打破您的假设。
- 有更好的方法可以使用异步(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
是安全的。
当然这种行为取决于具体的实现,但它应该是安全的,在UdpClient
, TcpClient
, Socket
等中确实如此……
由于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的实现,那么调用它将是安全的,并且在其余部分应该是安全的。
-
“除了
Dispose()
之外,考虑提供方法Close()
,如果close是区域中的标准术语。这样做时,将Close
实现与Dispose
相同并考虑明确地实现IDisposable.Dispose
方法是很重要的。 “ -
“从处理对象后无法使用的任何成员抛出
ObjectDisposedException
” 。 -
“要取消对
BeginConnect
方法的挂起调用,请关闭Socket
。当异步操作正在进行时调用Close
方法时,将调用提供给BeginConnect
方法的回调。”