可以无休止地异步工作流溢出堆栈

时间:2019-01-19 00:14:04

标签: asynchronous f#

假设我们有这个可能无限的工作流程:

let workAsync i = async { 
    printfn "Working... %A" i
    if i > 3 then
        failwith "errg"
    elif i = -1000 then // ensure work is async
        do! Async.Sleep 0
    return i+1
}

let workflow =
    async {
        let mutable i = 0
        while true do           // I can't quit you!
            let! j = workAsync i
            i <- j
            //do! Async.Sleep 0 // This is important?
    } |> Async.RunSynchronously

如果运行此命令,我们将得到预期的异常。 注意堆栈跟踪的增长方式。可以将其增大。

Working... 0
Working... 1
Working... 2
Working... 3
Working... 4
> System.Exception: errg
  at FSI_0017.workAsync@155-45.Invoke(Unit unitVar) in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 157
  at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2)
  at Microsoft.FSharp.Control.AsyncPrimitives.unitAsync@607.Invoke(AsyncActivation`1 ctxt)
  at FSI_0017.workAsync@158-47.Invoke(AsyncActivation`1 ctxt) in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 158
  at Microsoft.FSharp.Control.AsyncPrimitives.unitAsync@607.Invoke(AsyncActivation`1 ctxt)
  at Microsoft.FSharp.Control.AsyncPrimitives.unitAsync@607.Invoke(AsyncActivation`1 ctxt)
  at FSI_0017.workAsync@158-47.Invoke(AsyncActivation`1 ctxt) in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 158
  at Microsoft.FSharp.Control.AsyncPrimitives.unitAsync@607.Invoke(AsyncActivation`1 ctxt)
  at Microsoft.FSharp.Control.AsyncPrimitives.unitAsync@607.Invoke(AsyncActivation`1 ctxt)
  at FSI_0017.workAsync@158-47.Invoke(AsyncActivation`1 ctxt) in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 158
  at Microsoft.FSharp.Control.AsyncPrimitives.unitAsync@607.Invoke(AsyncActivation`1 ctxt)
  at Microsoft.FSharp.Control.AsyncPrimitives.unitAsync@607.Invoke(AsyncActivation`1 ctxt)
  at FSI_0017.workAsync@158-47.Invoke(AsyncActivation`1 ctxt) in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 158
  at Microsoft.FSharp.Control.AsyncPrimitives.unitAsync@607.Invoke(AsyncActivation`1 ctxt)
  at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction)
--- End of stack trace from previous location where exception was thrown ---
  at Microsoft.FSharp.Control.AsyncResult`1.Commit()
  at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronouslyInAnotherThread[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
  at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronously[T](CancellationToken cancellationToken, FSharpAsync`1 computation, FSharpOption`1 timeout)
  at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
  at <StartupCode$FSI_0017>.$FSI_0017.main@() in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 161

但是,如果我们取消注释Async.Sleep行,则堆栈不会增长:

Working... 0
Working... 1
Working... 2
Working... 3
Working... 4
> System.Exception: errg
   at FSI_0002.workAsync@155.Invoke(Unit unitVar) in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 157
   at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2)
   at FSI_0002.workflow@167-5.Invoke(AsyncActivation`1 ctxt) in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 167
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction)
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.FSharp.Control.AsyncResult`1.Commit()
   at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronouslyInAnotherThread[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
   at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronously[T](CancellationToken cancellationToken, FSharpAsync`1 computation, FSharpOption`1 timeout)
   at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
   at <StartupCode$FSI_0002>.$FSI_0002.main@() in C:\work\website_tq\tqit7\fs\Scripts\fsi_basic.fsx:line 161

*修改:* 更改了workAsync,以确保异步性没有得到优化。

1 个答案:

答案 0 :(得分:2)

之所以会发生这种情况,是因为当Sleep被注释掉时,您的工作流程中实际上没有异步。

一切都是完全同步的,但是由于它是在async计算表达式中编码的,因此它变得怪异地嵌套了。可见,每一行let!实际上都会调用右侧的任何内容(在您的示例中为workAsync),并向其传递一个回调,一旦完成异步部分,就需要调用该回调。回调包含其余代码-在let!行之后立即开始的继续。编译器对代码执行巧妙的转换,以使其看起来既美观又线性,而实际上却是一系列回调。

但是,由于workAsync实际上不是异步的,因此它会立即调用回调,然后回调转而调用workAsync的下一个迭代,依此类推。这样您的堆栈就会增加。

但是等等!毕竟它实际上不应该增长。回调的调用是workAsync中的最后一个调用(也称为“尾调用”),并且.NETCore和.NET Framework都消除了这些调用(实际上:在我的机器上,我无法重现您的结果)。我唯一可以提供的推测是,您必须在Mono上运行此代码,但这并不能总是消除尾声。

但是,如果您取消注释Sleep,则它将成为断点。 Sleep实际上是异步的,这意味着它计划在超时后在新线程上执行回调。该执行从头开始,使用新堆栈,因此即使不消除尾部调用,堆栈也不会增长。

所以要回答您的原始问题:不,无尽的异步计算不会溢出堆栈,除非它实际上不是异步的并且在Mono上运行。