简体   繁体   English

与 MailboxProcessor 和 Task 的交互永远挂起

[英]Interaction with MailboxProcessor and Task hangs forever

I want to process a series of jobs in sequence, but I want to queue up those jobs in parallel.我想按顺序处理一系列作业,但我想将这些作业并行排队。

Here is my code:这是我的代码:

open System.Threading.Tasks

let performWork (work : int) =
  task {
    do! Task.Delay 1000

    if work = 7 then
      failwith "Oh no"
    else
      printfn $"Work {work}"
  }

async {
  let w = MailboxProcessor.Start (fun inbox -> async {
    while true do
      let! message = inbox.Receive()

      let (ch : AsyncReplyChannel<_>), work = message

      do!
        performWork work
        |> Async.AwaitTask

      ch.Reply()
  })

  w.Error.Add(fun exn -> raise exn)

  let! completed =
    seq {
      for i = 1 to 10 do
        async {
          do! Async.Sleep 100
          do! w.PostAndAsyncReply(fun ch -> ch, i)

          return i
        }
    }
    |> fun jobs -> Async.Parallel(jobs, maxDegreeOfParallelism = 4)

  printfn $"Completed {Seq.length completed} job(s)."
}
|> Async.RunSynchronously

I expect this code to crash once it reaches work item 7 .我希望这段代码在到达工作项7时崩溃。

However, it hangs forever:但是,它永远挂起:

$ dotnet fsi ./Test.fsx
Work 3
Work 1
Work 2
Work 4
Work 5
Work 6

I think that the w.Error event is not firing correctly.我认为w.Error事件没有正确触发。

How should I be capturing and re-throwing this error?我应该如何捕获并重新抛出此错误?

If my work is async , then it crashes as expected:如果我的工作是async的,那么它会按预期崩溃:

let performWork (work : int) =
  async {
    do! Async.Sleep 1000

    if work = 7 then
      failwith "Oh no"
    else
      printfn $"Work {work}"
  }

But I don't see why this should matter.但我不明白为什么这很重要。


Leveraging a Result also works, but again, I don't know why this should be required.利用Result也可以,但同样,我不知道为什么需要这样做。

async {
  let w = MailboxProcessor.Start (fun inbox -> async {
    while true do
      let! message = inbox.Receive()

      let (ch : AsyncReplyChannel<_>), work = message

      try
        do!
          performWork work
          |> Async.AwaitTask

        ch.Reply(Ok ())
      with exn ->
        ch.Reply(Error exn)
  })

  let performWorkOnWorker (work : int) =
    async {
      let! outcome = w.PostAndAsyncReply(fun ch -> ch, work)

      match outcome with
      | Ok () ->
        return ()
      | Error exn ->
        return raise exn
    }

  let! completed =
    seq {
      for i = 1 to 10 do
        async {
          do! Async.Sleep 100
          do! performWorkOnWorker i

          return i
        }
    }
    |> fun jobs -> Async.Parallel(jobs, maxDegreeOfParallelism = 4)

  printfn $"Completed {Seq.length completed} job(s)."
}
|> Async.RunSynchronously

I think the problem is in your error handling:我认为问题出在您的错误处理中:

w.Error.Add(fun exn -> raise exn)

Instead of handling the exception, you're attempting to raise it again, which I think is causing an infinite loop.您没有处理异常,而是试图再次引发它,我认为这会导致无限循环。

You can change this to print the exception instead:您可以更改它以打印异常:

w.Error.Add(printfn "%A")

Result is:结果是:

Work 4
Work 2
Work 1
Work 3
Work 5
Work 6
System.AggregateException: One or more errors occurred. (Oh no)
 ---> System.Exception: Oh no
   at Program.performWork@4.MoveNext() in C:\Users\Brian Berns\Source\Repos\FsharpConsole\FsharpConsole\Program.fs:line 8
   --- End of inner exception stack trace ---

I think the gist of the 'why' here is that Microsoft changed the behaviour for 'unobserved' task exceptions back in .NET 4.5, and this was brought through into .NET Core: these exceptions no longer cause the process to terminate, they're effectively ignored.我认为这里“为什么”的要点是微软在 .NET 4.5 中改变了“未观察到”任务异常的行为,并将其引入 .NET 核心:这些异常不再导致进程终止,它们实际上是忽略。 You can read more about it here .您可以在此处阅读更多相关信息。

I don't know the ins and outs of how Task and async are interoperating, but it would seem that the use of Task results in the continuations being attached to that and run on the TaskScheduler as a consequence.我不知道Taskasync如何互操作的来龙去脉,但似乎使用Task会导致继续附加到它并因此在TaskScheduler上运行。 The exception is thrown as part of the async computation within the MailboxProcessor , and nothing is 'observing' it.该异常作为MailboxProcessorasync计算的一部分被抛出,没有任何东西“观察”它。 This means the exception ends up in the mechanism referred to above, and that's why your process no longer crashes.这意味着异常以上述机制结束,这就是您的进程不再崩溃的原因。

You can change this behaviour via a flag on .NET Framework via app.config , as explained in the link above.您可以通过 .NET Framework 上的标志通过app.config更改此行为,如上面的链接中所述。 For .NET Core, you can't do this.对于 .NET Core,你不能这样做。 You'd ordinarily try and replicate this by subscribing to the UnobservedTaskException event and re-throwing there, but that won't work in this case as the Task is hung and won't ever be garbage collected.您通常会尝试通过订阅UnobservedTaskException事件并在那里重新抛出来复制它,但是在这种情况下这将不起作用,因为Task已挂起并且永远不会被垃圾收集。

To try and prove the point, I've amended your example to include a timeout for PostAndReplyAsync .为了尝试证明这一点,我修改了您的示例以包含PostAndReplyAsync的超时。 This means that the Task will eventually complete, can be garbage collected and, when the finaliser runs, the event fired.这意味着Task最终完成,可以被垃圾收集,并且当终结器运行时,事件将被触发。

open System
open System.Threading.Tasks

let performWork (work : int) =
  task {
    do! Task.Delay 1000

    if work = 7 then
      failwith "Oh no"
    else
      printfn $"Work {work}"
  }

let worker = async {
  let w = MailboxProcessor.Start (fun inbox -> async {
    while true do
      let! message = inbox.Receive()

      let (ch : AsyncReplyChannel<_>), work = message

      do!
        performWork work
        |> Async.AwaitTask

      ch.Reply()
  })

  w.Error.Add(fun exn -> raise exn)

  let! completed =
    seq {
      for i = 1 to 10 do
        async {
          do! Async.Sleep 100
          do! w.PostAndAsyncReply((fun ch -> ch, i), 10000)

          return i
        }
    }
    |> fun jobs -> Async.Parallel(jobs, maxDegreeOfParallelism = 4)

  printfn $"Completed {Seq.length completed} job(s)."

}

TaskScheduler.UnobservedTaskException.Add(fun ex ->
    printfn "UnobservedTaskException was fired, re-raising"
    raise ex.Exception)

try
  Async.RunSynchronously worker
with
  | :? TimeoutException -> ()

GC.Collect()
GC.WaitForPendingFinalizers()

The output I get here is:我这里的output是:

Work 1
Work 3
Work 4
Work 2
Work 5
Work 6
UnobservedTaskException was fired, re-raising
Unhandled exception. System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. (One or more errors occurred. (Oh no))
 ---> System.AggregateException: One or more errors occurred. (Oh no)
 ---> System.Exception: Oh no
   at Program.performWork@5.MoveNext() in /Users/cmager/dev/ConsoleApp1/ConsoleApp2/Program.fs:line 9
   --- End of inner exception stack trace ---
   at Microsoft.FSharp.Control.AsyncPrimitives.Start@1078-1.Invoke(ExceptionDispatchInfo edi)
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 104
   at Microsoft.FSharp.Control.AsyncPrimitives.AttachContinuationToTask@1144.Invoke(Task`1 completedTask) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 1145
   at System.Threading.Tasks.ContinuationTaskFromResultTask`1.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__272_0(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
   --- End of inner exception stack trace ---
   at Program.clo@46-4.Invoke(UnobservedTaskExceptionEventArgs ex) in /Users/cmager/dev/ConsoleApp1/ConsoleApp2/Program.fs:line 48
   at Microsoft.FSharp.Control.CommonExtensions.SubscribeToObservable@1989.System.IObserver<'T>.OnNext(T args) in D:\a\_work\1\s\src\fsharp\FSharp.Core\async.fs:line 1990
   at Microsoft.FSharp.Core.CompilerServices.RuntimeHelpers.h@379.Invoke(Object _arg1, TArgs args) in D:\a\_work\1\s\src\fsharp\FSharp.Core\seqcore.fs:line 379
   at Program.clo@46-3.Invoke(Object delegateArg0, UnobservedTaskExceptionEventArgs delegateArg1) in /Users/cmager/dev/ConsoleApp1/ConsoleApp2/Program.fs:line 46
   at System.Threading.Tasks.TaskScheduler.PublishUnobservedTaskException(Object sender, UnobservedTaskExceptionEventArgs ueea)
   at System.Threading.Tasks.TaskExceptionHolder.Finalize()

As you can see, the exception is eventually published by the Task finaliser, and re-throwing it in that handler brings down the app.如您所见,异常最终由Task终结器发布,并在该处理程序中重新抛出它会导致应用程序关闭。

While interesting, I'm not sure any of this is practically useful information.虽然很有趣,但我不确定这些信息是否实用。 The suggestion to terminate the app within MailboxProcessor.Error handler is probably the right one.MailboxProcessor.Error处理程序中终止应用程序的建议可能是正确的。

As far as I see, when you throw an exception in the MailboxProcessor Body.据我所知,当您在 MailboxProcessor 主体中抛出异常时。 Then the MailboxProcessor doesn't hang forever, it just stops the whole MailboxProcessor.然后 MailboxProcessor 不会永远挂起,它只会停止整个 MailboxProcessor。

Your program also hangs, well because you do a Async.Parallel and wait until every async finished.您的程序也挂起,因为您执行了Async.Parallel并等待每个异步完成。 But those with an exception, never finish, or returns a result.但是那些有异常的,永远不会完成,或者返回结果。 So your program overall, hangs forever.因此,您的程序总体上会永远挂起。

If you want to explicitly abort, then you need to call System.Environment.Exit , not just throw an exception.如果要显式中止,则需要调用System.Environment.Exit ,而不仅仅是抛出异常。

One way to re-write your program is like this.重写程序的一种方法是这样的。

open System.Threading.Tasks

let performWork (work : int) = task {
    do! Task.Delay 1000

    if   work = 7
    then failwith "Oh no"
    else printfn $"Work {work}"
}

let mb =
    let mbBody (inbox : MailboxProcessor<AsyncReplyChannel<_> * int>) = async {
        while true do
            let! (ch,work) = inbox.Receive()
            try
                do! performWork work |> Async.AwaitTask
                ch.Reply ()
            with error  ->
                System.Environment.Exit 0
    }
    MailboxProcessor.Start mbBody

Async.RunSynchronously (async {
    let! completed =
        let jobs = [|
            for i = 1 to 10 do
                async {
                    do! Async.Sleep 100
                    do! mb.PostAndAsyncReply(fun ch -> ch, i)
                    return i
                }
        |]
        Async.Parallel(jobs)

    printfn $"Completed {Seq.length completed} job(s)."
})

Btw.顺便提一句。 i changed the seq {} to an array, and additional removed the maxDegreeOfParallelism option.我将seq {}更改为数组,并额外删除了maxDegreeOfParallelism选项。 Otherwise the results seemed not to be very parallel in my tests.否则,结果在我的测试中似乎不是很平行 But you still can keep those if you want.但如果你愿意,你仍然可以保留那些。

executing this program prints something like:执行这个程序会打印出如下内容:

Work 10
Work 4
Work 9
Work 3
Work 8

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM