可以在“使用”语句中使用异步方法的结果,例如:
using (await fooAsync())
{
...
}
不幸的是,很容易犯这个错误:
using (fooAsync())
{
...
}
一旦您犯了这个错误,就很难检测到。如果您找回的任务恰好完成,则Task.Dispose将成功完成。事实证明,Task.Dispose does not call Task.Result.Dispose也是如此,因此您实际上想通过“ using”语句保护的对象仍然悬空。
作为fooAsync的作者,防止错误被静默忽略的最佳方法是什么?
答案 0 :(得分:2)
您只需要小心。
虽然Visual Studio通常会在不等待常规代码中的Task
的情况下发出警告,但如果它是using
的主题,则不会发出警告。例如:
请注意,using
行上没有弯曲。
正如您提到的,Task
确实实现了IDisposable
,所以这是完全有效的代码,这使得很难说这是否真的是一个错误。您随时可以log an issue只是开始讨论。
也就是说,如果您查看Task.Dispose()
的代码,则如果throw an exception在完成之前就已经处理掉了,则会执行Nito.AsyncEx
。
// Task must be completed to dispose
if (!IsCompleted)
{
throw new InvalidOperationException(Environment.GetResourceString("Task_Dispose_NotCompleted"));
}
因此,尽管不能保证,但是很有可能在测试是否忘记等待时会被该异常击中。
如果您不使用using
内的一次性对象,这实际上只是一个问题,这非常少见。否则,您很快就会发现:
using (var foo = FooAsync()) {
foo.WhyDoesNothingWork();
}
但是,正如您所指出的,在不使用块内一次性对象的情况下,非常合理的用法是使用异步锁,例如AwaitableDisposable
:
using (await _mutex.LockAsync()) {
// do stuff
}
事实证明,史蒂芬·克莱里(Stephen Cleary)创建了一个The Issue With Scoped Async Synchronization Constructs类来解决这个问题。请参阅该课程顶部的评论:
一个等待任务的包装,其结果是一次性的。包装器不是一次性的,因此当适当的用法应为“使用(await MyAsync())”时,这样可以防止出现诸如“使用(MyAsync())”之类的使用错误。
答案 1 :(得分:0)
如果要使用Roslyn Analyzer方法,则以下内容足以使您入门。这将检查using块中没有等待的任何调用,确定它们是否为通用任务,并检查其通用任务参数是否为IDisposable或实现IDisposable。
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsyncUsingAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "UnAwaitedTaskInUsing";
private const string Title = "Await Async Method";
private const string MessageFormat = "{0} should be awaited";
private const string Description = "Detected un-awaited task in using.";
private const string Category = "Usage";
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeSymbol, SyntaxKind.UsingStatement);
}
private static void AnalyzeSymbol(SyntaxNodeAnalysisContext context)
{
var invocationExpression = context.Node.ChildNodes().OfType<InvocationExpressionSyntax>().FirstOrDefault();
if(invocationExpression == null) return;
var awaitKeyword = context.Node.ChildTokens().OfType<AwaitExpressionSyntax>().FirstOrDefault();
if(awaitKeyword != null) return;
var symbolInfo = context.SemanticModel.GetSymbolInfo(invocationExpression).Symbol as IMethodSymbol;
if(symbolInfo == null) return;
var genericType = (symbolInfo.ReturnType as INamedTypeSymbol);
if (!genericType?.IsGenericType ?? false || genericType.Name.ToString() != "Task'1")
return;
var genericTypeParameter = genericType.TypeArguments.FirstOrDefault();
if(genericTypeParameter == null)
return;
var disposable = context.Compilation.GetTypeByMetadataName("System.IDisposable");
if(!disposable.Equals(genericTypeParameter) && !genericTypeParameter.Interfaces.Any(x => disposable.Equals(x)))
return;
var diagnostic = Diagnostic.Create(Rule, invocationExpression.GetLocation(), invocationExpression);
context.ReportDiagnostic(diagnostic);
}
}
当前,严重性设置为“警告”,但是如果有人忘记等待任务,您可以很容易地将此错误设置为在编译时抛出的错误。
我不会认为这是一个完整的解决方案,但是它足以让您入门。