【问题标题】:F# Async Equivalent of Task.ContinueWithTask.ContinueWith 的 F# 异步等效项
【发布时间】:2019-03-21 19:54:12
【问题描述】:

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

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

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

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()

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

【问题讨论】:

    标签: f# fody continuewith async-workflow


    【解决方案1】:

    你可能需要这样的东西:

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

    请考虑,当函数返回异步时,并不意味着异步已启动。异步更像是一个函数,它可以被调用多次或根本不调用。这意味着您还需要在 OnEntry 方法中检查返回值是否为 Async。

    【讨论】:

    • 谢谢,这让我走上了正轨。我实际上想捕获从调用函数到异步执行完成的整个时间,所以我保留了我的 OnEntry 行为,但是使用这样的函数并在我的 OnExit 中反射性地调用它绝对是改进。
    【解决方案2】:

    按照@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 实现保持不变。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-22
      • 2014-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-12
      相关资源
      最近更新 更多