在使用异步编程模型时,通常建议每个
BeginXXX
与
EndXXX
相匹配,否则您可能会泄漏在异步操作仍在“运行”时保留的资源。如果类实现了
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()
{
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);
}
private void HandleMessage(IAsyncResult iar)
{
try
{
IPEndPoint remoteEP = null;
Byte[] buffer = _udpClient.EndReceive(iar, ref remoteEP);
_processor(new UdpReceiveResult(buffer, remoteEP));
BeginReceive();
}
catch (ObjectDisposedException)
{
_tcs.SetResult(true);
}
catch (Exception ex)
{
_tcs.TrySetException(ex);
}
}
}
_udpClient
实例上调用EndReceive
,它将仅抛出一个ObjectDisposedException
而不做任何其他操作。因此,不,您不应该这样做,尽管我很难想象在这种情况下何时/何地会调用EndReceive
。 - Alex