如何处理异步“使用”语句陷阱?

时间:2019-11-26 14:41:32

标签: c# async-await using

可以在“使用”语句中使用异步方法的结果,例如:

using (await fooAsync())
{
    ...
}

不幸的是,很容易犯这个错误:

using (fooAsync())
{
    ...
}

一旦您犯了这个错误,就很难检测到。如果您找回的任务恰好完成,则Task.Dispose将成功完成。事实证明,Task.Dispose does not call Task.Result.Dispose也是如此,因此您实际上想通过“ using”语句保护的对象仍然悬空。

作为fooAsync的作者,防止错误被静默忽略的最佳方法是什么?

2 个答案:

答案 0 :(得分:2)

您只需要小心。

虽然Visual Studio通常会在不等待常规代码中的Task的情况下发出警告,但如果它是using的主题,则不会发出警告。例如:

Lacking await

请注意,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())”之类的使用错误。

我在这里找到了同一件事的另一种实现:Model Summary

答案 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);
    }
}

当前,严重性设置为“警告”,但是如果有人忘记等待任务,您可以很容易地将此​​错误设置为在编译时抛出的错误。

我不会认为这是一个完整的解决方案,但是它足以让您入门。