[英]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.我不知道
Task
和async
如何互操作的来龙去脉,但似乎使用Task
会导致继续附加到它并因此在TaskScheduler
上运行。 The exception is thrown as part of the async
computation within the MailboxProcessor
, and nothing is 'observing' it.该异常作为
MailboxProcessor
中async
计算的一部分被抛出,没有任何东西“观察”它。 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.