Task.ContinueWith 的 F# 异步等效项

F# Async Equivalent of Task.ContinueWith

我一直在为我们的一些较大的 .NET 解决方案实施 [<Trace>] 属性,这将允许将可配置的分析轻松添加到任何被认为重要的 functions/methods 中。我正在使用 Fody 和 MethodBoundaryAspect 拦截每个函数的进入和退出并记录指标。这适用于同步函数,对于 return Task 的方法,Task.ContinueWith 有一个可行的解决方案,但对于 F# Async-returning 函数,OnExit 来自 MethodBoundaryAspect 的运行在 Async 被 returned 时立即运行(而不是在实际执行 Async 时)。

为了捕获 F# Async-returning 函数的正确指标,我试图想出一个与使用 Task.ContinueWith 等效的解决方案,但我能想到的最接近的事情是创建一个绑定第一个异步的新异步,运行度量捕获函数,然后 returns 原始结果。由于我拦截的 F# Async return 值仅作为 obj 呈现,因此我必须反思地做所有事情,因为没有非通用版本Async 就像 Task 一样,我可以在不知道确切的 return 类型的情况下使用它。

到目前为止,我最好的解决方案大致如下:

open System
open System.Diagnostics
open FSharp.Reflection
open MethodBoundaryAspect.Fody.Attributes

[<AllowNullLiteral>]
[<AttributeUsage(AttributeTargets.Method ||| AttributeTargets.Property, AllowMultiple = false)>]
type TraceAttribute () =
    inherit OnMethodBoundaryAspect()

    let traceEvent (args: MethodExecutionArgs) (timestamp: int64) =
        // Capture metrics here
        ()

    override __.OnEntry (args) =
        Stopwatch.GetTimestamp() |> traceEvent args

    override __.OnExit (args) =
        let exit () = Stopwatch.GetTimestamp() |> traceEvent args
        match args.ReturnValue with
        | :? System.Threading.Tasks.Task as task ->
            task.ContinueWith(fun _ -> exit()) |> ignore             
        | other -> // Here's where I could use some help
            let clrType = other.GetType()
            if clrType.IsGenericType && clrType.GetGenericTypeDefinition() = typedefof<Async<_>> then
                // If the return type is an F# Async, replace it with a new Async that calls exit after the original return value is computed
                let returnType = clrType.GetGenericArguments().[0]
                let functionType = FSharpType.MakeFunctionType(returnType, typedefof<Async<_>>.MakeGenericType([| returnType |]))
                let f = FSharpValue.MakeFunction(functionType, (fun _ -> exit(); other))
                let result = typeof<AsyncBuilder>.GetMethod("Bind").MakeGenericMethod([|returnType; returnType|]).Invoke(async, [|other; f|]) 
                args.ReturnValue <- result
            else
                exit()

不幸的是,这个解决方案不仅非常混乱,而且我相信异步计算的反射构造会增加大量的开销,尤其是当我试图跟踪在循环中调用的函数时或者有深层嵌套的异步调用。有没有更好的方法可以在实际评估异步计算后立即获得 运行 给定函数的相同结果?

您可能需要这样的东西:

let traceAsync (a:Async<_>) = async {
    trace() // trace start of async
    let! r = a
    trace() // trace end of async
    return r
}

考虑一下,当一个函数 return 是一个异步并不意味着异步已经开始。异步更像是一个函数,它可以被调用多次或 none 。这意味着您需要在 OnEntry 方法中检查 return 值是否也是 Async。

按照@AMieres 的建议,我能够更新我的 OnExit 方法以正确跟踪异步执行,而无需太多开销。我认为大部分问题实际上是在使用相同的 AsyncBuilder 实例时导致异步函数的额外调用。这是新的解决方案:

open System
open System.Diagnostics
open FSharp.Reflection
open MethodBoundaryAspect.Fody.Attributes

[<AllowNullLiteral>]
[<AttributeUsage(AttributeTargets.Method ||| AttributeTargets.Property, AllowMultiple = false)>]
type TraceAttribute () =
    inherit OnMethodBoundaryAspect()
    static let AsyncTypeDef = typedefof<Async<_>>
    static let Tracer = typeof<TraceAttribute>
    static let AsyncTracer = Tracer.GetMethod("TraceAsync")

    let traceEvent (args: MethodExecutionArgs) (timestamp: int64) =
        // Capture metrics here
        ()

    member __.TraceAsync (asyncResult: Async<_>) trace =
        async {
            let! result = asyncResult
            trace()
            return result
        }

    override __.OnEntry (args) =
        Stopwatch.GetTimestamp() |> traceEvent args

    override __.OnExit (args) =
        let exit () = Stopwatch.GetTimestamp() |> traceEvent args
        match args.ReturnValue with
        | :? System.Threading.Tasks.Task as task ->
            task.ContinueWith(fun _ -> exit()) |> ignore             
        | other -> 
            let clrType = other.GetType()
            if clrType.IsGenericType && clrType.GetGenericTypeDefinition() = AsyncTypeDef then
                let generics = clrType.GetGenericArguments()
                let result = AsyncTracer.MakeGenericMethod(generics).Invoke(this, [| other; exit |])
                args.ReturnValue <- result
            else
                exit()

这似乎以更少的开销正确跟踪异步函数。我确实想跟踪调用函数的总时间,而不是异步实际开始的时间,所以我让 OnEntry 实现保持不变。