【发布时间】:2016-05-31 10:27:15
【问题描述】:
当检测到CancellationToken.IsCancelRequested 时,通过抛出OperationCancelledException 以外的其他内容让我的库打破方法是不好的做法吗?
例如:
async Task<TcpClient> ConnectAsync(string host, int port, CancellationToken ct)
{
var client = new TcpClient();
try
{
using (ct.Register(client.Close, true))
{
await client.ConnectAsync(host, port);
}
// Pick up strugglers here because ct.Register() may have hosed our client
ct.ThrowIfCancellationRequested();
}
catch (Exception)
{
client.Close();
throw;
}
return client;
}
取消后,这有可能根据时间抛出ObjectDisposedException 或NullReferenceException。 (因为TcpClient.ConnectAsync() 可以在同时调用TcpClient.Close() 时抛出任何一个。)
现在,我可以像这样修复:
async Task<TcpClient> ConnectAsync(string host, int port, CancellationToken ct)
{
var client = new TcpClient();
try
{
using (ct.Register(client.Close, true))
{
try
{
await client.ConnectAsync(host, port);
}
catch (Exception)
{
// These exceptions are likely because we closed the
// connection with ct.Register(). Convert them to
// OperationCancelledException if that's the case
ct.ThrowIfCancellationRequested();
throw;
}
}
// Pick up strugglers here because ct.Register() may have hosed our client
ct.ThrowIfCancellationRequested();
}
catch (Exception)
{
client.Close();
throw;
}
return client;
}
同样适用于调用层次结构的每一层:
async Task<TcpClient> ConnectSslStreamAsync(string host, int port, CancellationToken ct)
{
var client = await ConnectAsync(host, port, ct);
try
{
ct.ThrowIfCancellationRequested();
var sslStream = new SslStream(client.getStream());
using (ct.Register(sslStream.Close))
{
try
{
await sslStream.AuthenticateAsClientAsync(...);
}
catch (Exception)
{
// These exceptions are likely because we closed the
// stream with ct.Register(). Convert them to
// OperationCancelledException if that's the case
ct.ThrowIfCancellationRequested();
throw;
}
}
// Pick up strugglers here because ct.Register() may have hosed our stream
ct.ThrowIfCancellationRequested();
}
catch (Exception)
{
client.Close();
throw;
}
return client;
}
但这会添加这些额外的代码,而实际上,只需要在最外层进行一次检查即可。 (在取消源处。)
让这些任意异常发生是不好的做法吗?还是在最外层的调用者常规忽略它们?
【问题讨论】:
-
当微软显式添加一个引发异常的方法时,不,“坏习惯”并不是首先想到的。只是假装它没有被抛出是一种不好的做法。
-
我什至不会把
ct.ThrowIfCancellationRequested()放在ConnectAsync的末尾。如果令牌在此调用之后但在ConnectAsync返回之前被取消怎么办?我宁愿让图书馆的客户处理各种赛车异常,只记录可能的副作用。 -
@Noseratio
ct.ThrowIfCancellationRequested()之所以存在,是因为没有它,using (ct.Register(...))可以关闭打开的连接,即使我们的ConnectAsync()继续成功。在我们的示例中它可能没有什么不同(调用者可能无论如何都会处理连接),但作为经验法则,该结构可能值得保留,以在我们的方法继续执行的情况下保持完整性(原子性)改变状态。 -
为什么要调用 ct.ThrowIfCancellationRequested();等待之后。它解决了什么目的?
-
@Saravanan 你是说
ConnectSslStreamAsync()的例子吗?在await之后,在更多工作之前,不是最标准的放置它们的地方之一吗? (因为在异步中断之后等待恢复,而世界大部分地区可能在此期间发生了变化。)
标签: c# async-await