问题描述
使用异步编程模型时 通常建议将每个 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 IDisposable
s) 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.
推荐答案
如果类实现了 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:
- 调用
EndXXX
after 处理UDPClient
实例将导致它直接抛出ObjectDisposedException
. - 没有资源在
EndXXX
调用中或作为其结果处理. - 与此操作相关的资源(本机重叠和固定的非托管缓冲区)将在异步操作完成回调中回收.
- Calling
EndXXX
after disposing theUDPClient
instance will result in it directly throwing anObjectDisposedException
. - No resources are disposed in or as a result of the
EndXXX
call. - 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:
- 实施可能会在未来发生变化,从而打破您的假设.
- 有更好的方法来做到这一点,对异步 (I/O) 操作使用取消和超时(例如,通过在
_udpClient
实例上调用Close
强制 I/O 失败).
- The implementation could change in the future, breaking your assumptions.
- 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 委托调用.
以下是基础Socket.BeginReceive
方法:
And the following for the underlying Socket.BeginReceive
method:
异步BeginReceive
操作必须通过调用EndReceive
方法来完成.通常,该方法由回调委托调用.
要取消挂起的 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 是否安全的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!