使用Asynchronous Programming Model时,通常建议将每个BeginXXX
与EndXXX
匹配,否则在异步操作完成之前可能会泄漏资源。
如果类实现IDisposable
并且通过调用Dispose
处理了实例,情况仍然如此吗?
例如,如果我在UdpClient.BeginReceive
中使用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.EndReceive
上调用_udpClient
(这只会产生ObjectDisposedException
)?
修改
在完成所有异步操作之前配置UdpClient
(和其他IDisposable
s)作为取消或实现超时的方法并不罕见,特别是在operations that will never complete之上。这也是建议的throughout this site。
答案 0 :(得分:4)
使用异步编程模型时,通常建议将每个
BeginXXX
与EndXXX
进行匹配,否则在异步操作仍处于“运行”状态时,可能会有泄漏资源的风险。如果类实现
IDisposable
并且在实例上调用Dispose
,情况仍然如此吗?
这与实现IDisposable
的类无关。
除非您可以确定异步完成将释放与通过BeginXXX
启动的异步操作相关的任何资源,并且不会执行清除,或者EndXXX
调用,您需要确保匹配您的电话。 某些的唯一方法是检查特定异步操作的实现。
对于您选择的 UdpClient
示例,恰好是这样的情况:
EndXXX
实例后调用UDPClient
会导致它直接抛出ObjectDisposedException
。EndXXX
电话中没有处理任何资源。所以在这种情况下,它是完全安全的,没有泄漏。
作为一般方法
我不相信这种方法作为一般方法是正确的,因为:
Close
实例上的_udpClient
来强制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);
}
}
}
答案 1 :(得分:0)
考虑到事实Dispose
(应该与Close
1 相同)释放任何非托管资源(GC释放托管资源)和方法抛出{{1当在已处理的实例 2 上调用时,应安全,不能调用ObjectDisposedException
。
这种行为当然取决于具体的实施方式,但它应该是安全的,EndXXX
,UdpClient
,TcpClient
等等确实如此......
由于APM早于TPL和随附的Socket
,因此您通常无法使用CancelationToken
取消这些异步操作。这就是为什么你也无法通过等效的CancelationToken
方法(例如UdpClient.RecieveAsync
)传递CancelationToken
,因为它们只是wrapper over the BeginXXX
/EndXXX
methods with a call to Task.Factory.FromAsync
。此外,超时(如Socket.ReceiveTimeout
)通常只会影响同步选项,而不会影响异步选项。
取消此类操作的唯一方法是通过部署实例本身 3 来释放所有资源并调用所有等待的回调,这些回调通常会调用async-await
并获得相应的EndXXX
。在处理实例时,通常会从这些方法的第一行引发此异常。
根据我们对APM的了解和ObjectDisposedException
来电IDisposable
应该足以清除任何悬挂的资源并添加对Dispose
的调用只会引起无用的EndXXX
仅此而已。调用ObjectDisposedException
可能会保护您开发人员不遵守指南的地方(可能不会,这取决于错误的实施)但是如果不是全部的话,那么调用它将是安全的.Net&#39;实施,其余部分应该是安全的。