[英]F# MailboxProcessor - multiple waiting reader continuations for mailbox
我正在玩寫一些非常簡單的異步測試框架。 但我認為我正在遇到某種限制或錯誤。 抱歉,我無法在較小的代碼庫上重現這一點。
這是我提出的基本框架:
module TestRunner
open System
type TestOptions = {
Writer : ConsoleColor -> string -> unit}
type TestResults = {
Time : TimeSpan
Failure : exn option
}
type Test = {
Name : string
Finished : IEvent<TestResults>
SetFinished : TestResults -> unit
TestFunc : TestOptions -> Async<TestResults> }
let createTest name f =
let ev = new Event<TestResults>()
{
Name = name
Finished = ev.Publish
SetFinished = (fun res -> ev.Trigger res)
TestFunc =
(fun options -> async {
let watch = System.Diagnostics.Stopwatch.StartNew()
try
do! f options
watch.Stop()
return { Failure = None; Time = watch.Elapsed }
with exn ->
watch.Stop()
return { Failure = Some exn; Time = watch.Elapsed }
})}
let simpleTest name f =
createTest name (fun options -> f options.Writer)
/// Create a new Test and change the result
let mapResult mapping test =
{ test with
TestFunc =
(fun options -> async {
let! result = test.TestFunc options
return mapping result})}
let writeConsole color f =
let old = System.Console.ForegroundColor
try
System.Console.ForegroundColor <- color
f()
finally
System.Console.ForegroundColor <- old
let printColor color (text:String) =
writeConsole color (fun _ -> Console.WriteLine(text))
type WriterMessage =
| NormalWrite of ConsoleColor * String
| StartTask of AsyncReplyChannel<int> * String
| WriteMessage of int * ConsoleColor * String
| EndTask of int
/// will handle printing jobs for two reasons
/// 1. Nice output grouped by tests (StartTask,WriteMessage,EndTask)
/// 2. Print Summary after all tests finished (NormalWrite)
let writer = MailboxProcessor.Start (fun inbox ->
let currentTask = ref 0
let newHandle (returnHandle:AsyncReplyChannel<int>) =
let handle = System.Threading.Interlocked.Increment currentTask
returnHandle.Reply handle
handle
// the tasks describe which tasks are currently waiting to be processed
let rec loop tasks = async {
let! newTasks =
match tasks with
/// We process the Task with the number t and the name name
| (t, name) :: next ->
inbox.Scan
(fun msg ->
match msg with
| EndTask (endTask) ->
// if the message is from the current task finish it
if t = endTask then
Some (async { return next })
else None
| WriteMessage(writeTask, color, message) ->
if writeTask = t then
Some (async {
printColor color (sprintf "Task %s: %s" name message)
return tasks
})
else None
| StartTask (returnHandle, name) ->
// Start any tasks instantly and add them to the list (because otherwise they would just wait for the resonse)
Some (async {
let handle = newHandle returnHandle
return (List.append tasks [handle, name]) })
| _ -> None)
// No Current Tasks so just start ones or process the NormalWrite messages
| [] ->
inbox.Scan
(fun msg ->
match msg with
| StartTask (returnHandle, name) ->
Some (async {
let handle = newHandle returnHandle
return [handle, name] })
| NormalWrite(color, message) ->
Some (async {
printColor color message
return []
})
| _ -> None)
return! loop newTasks
}
loop [])
/// Write a normal message via writer
let writerWrite color (text:String) =
writer.Post(NormalWrite(color, text))
/// A wrapper around the communication (to not miss EndTask for a StartTask)
let createTestWriter name f = async {
let! handle = writer.PostAndAsyncReply(fun reply -> StartTask(reply, name))
try
let writer color s =
writer.Post(WriteMessage(handle,color,s))
return! f(writer)
finally
writer.Post (EndTask(handle))
}
/// Run the given test and print the results
let testRun t = async {
let! results = createTestWriter t.Name (fun writer -> async {
writer ConsoleColor.Green (sprintf "started")
let! results = t.TestFunc { Writer = writer }
match results.Failure with
| Some exn ->
writer ConsoleColor.Red (sprintf "failed with %O" exn)
| None ->
writer ConsoleColor.Green (sprintf "succeeded!")
return results})
t.SetFinished results
}
/// Start the given task with the given amount of workers
let startParallelMailbox workerNum f =
MailboxProcessor.Start(fun inbox ->
let workers = Array.init workerNum (fun _ -> MailboxProcessor.Start f)
let rec loop currentNum = async {
let! msg = inbox.Receive()
workers.[currentNum].Post msg
return! loop ((currentNum + 1) % workerNum)
}
loop 0 )
/// Runs all posted Tasks
let testRunner =
startParallelMailbox 10 (fun inbox ->
let rec loop () = async {
let! test = inbox.Receive()
do! testRun test
return! loop()
}
loop ())
/// Start the given tests and print a sumary at the end
let startTests tests = async {
let! results =
tests
|> Seq.map (fun t ->
let waiter = t.Finished |> Async.AwaitEvent
testRunner.Post t
waiter
)
|> Async.Parallel
let testTime =
results
|> Seq.map (fun res -> res.Time)
|> Seq.fold (fun state item -> state + item) TimeSpan.Zero
let failed =
results
|> Seq.map (fun res -> res.Failure)
|> Seq.filter (fun o -> o.IsSome)
|> Seq.length
let testCount = results.Length
if failed > 0 then
writerWrite ConsoleColor.DarkRed (sprintf "--- %d of %d TESTS FAILED (%A) ---" failed testCount testTime)
else
writerWrite ConsoleColor.DarkGray (sprintf "--- %d TESTS FINISHED SUCCESFULLY (%A) ---" testCount testTime)
}
現在只有在我使用一組特定的測試時才會觸發異常,這些測試會在Web上進行一些爬行(有些失敗,有些失敗則沒有問題):
#r @"Yaaf.GameMediaManager.Primitives.dll";; // See below
open TestRunner
let testLink link =
Yaaf.GameMediaManager.EslGrabber.getMatchMembers link
|> Async.Ignore
let tests = [
// Some working links (links that should work)
yield!
[ //"TestMatch", "http://www.esl.eu/eu/wire/anti-cheat/css/anticheat_test/match/26077222/"
"MatchwithCheater", "http://www.esl.eu/de/csgo/ui/versus/match/3035028"
"DeletedAccount", "http://www.esl.eu/de/css/ui/versus/match/2852106"
"CS1.6", "http://www.esl.eu/de/cs/ui/versus/match/2997440"
"2on2Versus", "http://www.esl.eu/de/css/ui/versus/match/3012767"
"SC2cup1on1", "http://www.esl.eu/eu/sc2/go4sc2/cup230/match/26964055/"
"CSGO2on2Cup", "http://www.esl.eu/de/csgo/cups/2on2/season_08/match/26854846/"
"CSSAwpCup", "http://www.esl.eu/eu/css/cups/2on2/awp_cup_11/match/26811005/"
] |> Seq.map (fun (name, workingLink) -> simpleTest (sprintf "TestEslMatches_%s" name) (fun o -> testLink workingLink))
]
startTests tests |> Async.Start;; // this will produce the Exception now and then
https://github.com/matthid/Yaaf.GameMediaManager/blob/core/src/Yaaf.GameMediaManager.Primitives/EslGrabber.fs是代碼,您可以下載https://github.com/downloads/matthid/Yaaf。 GameMediaManager / GameMediaManager。%200.9.3.1.wireplugin (這基本上是一個重命名的zip存檔)並解壓縮得到Yaaf.GameMediaManager.Primitives.dll二進制文件(你可以把它粘貼到FSI而不是你想要的下載但是你有引用HtmlAgilityPack)
我可以使用Microsoft(R)F#2.0 Interactive,Build 4.0.40219.1重現這一點。 問題是異常不會一直觸發(但經常)並且堆棧跟蹤不會告訴我什么
System.Exception: multiple waiting reader continuations for mailbox
bei <StartupCode$FSharp-Core>.$Control.-ctor@1860-3.Invoke(AsyncParams`1 _arg11)
bei <StartupCode$FSharp-Core>.$Control.loop@413-40(Trampoline this, FSharpFunc`2 action)
bei Microsoft.FSharp.Control.Trampoline.ExecuteAction(FSharpFunc`2 firstAction)
bei Microsoft.FSharp.Control.TrampolineHolder.Protect(FSharpFunc`2 firstAction)
bei <StartupCode$FSharp-Core>.$Control.finishTask@1280[T](AsyncParams`1 _arg3, AsyncParamsAux aux, FSharpRef`1 firstExn, T[] results, TrampolineHolder trampolineHolder, Int32 remaining)
bei <StartupCode$FSharp-Core>.$Control.recordFailure@1302[T](AsyncParams`1 _arg3, AsyncParamsAux aux, FSharpRef`1 count, FSharpRef`1 firstExn, T[] results, LinkedSubSource innerCTS, TrampolineHolder trampolineHolder, FSharpChoice`2 exn)
bei <StartupCode$FSharp-Core>.$Control.Parallel@1322-3.Invoke(Exception exn)
bei Microsoft.FSharp.Control.AsyncBuilderImpl.protectedPrimitive@690.Invoke(AsyncParams`1 args)
bei <StartupCode$FSharp-Core>.$Control.loop@413-40(Trampoline this, FSharpFunc`2 action)
bei Microsoft.FSharp.Control.Trampoline.ExecuteAction(FSharpFunc`2 firstAction)
bei Microsoft.FSharp.Control.TrampolineHolder.Protect(FSharpFunc`2 firstAction)
bei <StartupCode$FSharp-Core>.$Control.-ctor@473-1.Invoke(Object state)
bei System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
bei System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
bei System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
bei System.Threading.ThreadPoolWorkQueue.Dispatch()
bei System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
因為這將在我無法控制的工作線程上觸發,這將使應用程序崩潰(不是FSI,但此處也會顯示異常)。
我找到了http://cs.hubfs.net/topic/Some/2/59152和http://cs.hubfs.net/topic/None/59146,但我不使用StartChild,我不認為我是某種程度上同時調用從多個線程接收?
我的代碼有什么問題,或者這確實是一個錯誤? 如果可能,我該如何解決這個問題?
我注意到在FSI中,當默認忽略Exception時,所有測試都將按預期運行。 我怎么能這樣做?
編輯:我注意到我修復了失敗的單元測試后它會正常工作。 但是,我不能用較小的代碼庫重現這一點。 例如,我自己的失敗測試。
謝謝,matthid
我的感覺是限制將在MailboxProcessor本身而不是異步。
說實話,我會謹慎使用掃描功能。 我寫了一篇關於使用它們的危險的博客文章 。
是否可以使用標准接收機制而不是使用掃描功能來處理任務?
作為一個注釋,在異步內部有一個trampoline被使用,以便相同的線程被重用一段時間,以避免不必要的線程池使用,(我認為這設置為300)所以在調試時你可能會看到這種行為。
我會稍微區別地解決這個問題,將單獨的組件分解為管道階段而不是嵌套的異步塊。 我將創建一個supervisor組件和路由組件。
主管將負責初始測試並將消息發布到路由組件,該組件將請求循環到其他代理。 任務完成后,他們可以回復給主管。
我意識到這對當前代碼中的問題沒有任何幫助,但我認為無論如何都要分解問題以調試系統的異步部分。
我相信在Scan
/ TryScan
/ Receive
的2.0實現中有一個錯誤可能會導致錯誤
multiple waiting reader continuations for mailbox
例外; 我認為現在已經在3.0實現中修復了這個bug。 我沒有仔細查看您的代碼,以確保您在實現中一次只嘗試接收一條消息,因此這也可能是您的代碼中的錯誤。 如果你可以嘗試對抗F#3.0,那么很高興知道這是否會消失。
一些注意事項,以防有人發現我的經驗有用(調試多個進程需要很長時間才能找到問題):
只有50個代理/郵箱,執行和吞吐量開始變得堵塞。 有時輕負載它會對第一輪消息起作用,但是對調用日志庫的任何重要事件都會觸發更長的延遲。
使用VS IDE中的Threads / Parallel Stacks窗口進行調試,運行時正在等待FSharpAsync.RunSynchronously - > CancellationTokenOps.RunSynchronously調用Trampoline.ExecuteAction的結果
我懷疑底層的ThreadPool是限制啟動(在它第一次運行似乎運行正常之后)。 這是一個很長的延遲。 我正在使用代理在某些隊列中進行小規模計算,同時允許主調度代理保持響應,因此延遲在CLR中的某處。
我發現在try-with中運行帶有Timeout的MailboxProcessor Receive,停止了延遲,但是這需要包含在異步塊中以阻止程序的其余部分減慢,但是延遲時間很短。 盡管有點蠢蠢欲動,但對於實現actor模型的F#MailboxProcessor非常滿意。
遺憾的是,我從來沒有真正能夠在較小的代碼庫上重現這一點,現在我將使用NUnit與異步測試支持而不是我自己的實現。 我在各種項目中使用了代理(MailboxProcessor)和asyncs,從未再遇到過這種情況......
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.