[英]Use Post or PostAndAsyncReply with F#'s MailboxProcessor?
[英]How to use TryScan in F# properly
我試圖找到一個關於如何使用TryScan
的例子,但沒有找到任何,你能幫幫我嗎?
我想做什么(非常簡化的例子):我有一個MailboxProcessor
接受兩種類型的消息。
第一個GetState
返回當前狀態。 GetState
消息經常發送
另一個UpdateState
非常昂貴(耗時) - 例如從互聯網下載某些內容然后相應地更新狀態。 UpdateState
很少被調用。
我的問題是 - 消息GetState
被阻止,並等到服務於UpdateState
之前。 這就是我嘗試使用TryScan
處理所有GetState
消息的原因,但沒有運氣。
我的示例代碼:
type Msg = GetState of AsyncReplyChannel<int> | UpdateState
let mbox = MailboxProcessor.Start(fun mbox ->
let rec loop state = async {
// this TryScan doesn't work as expected
// it should process GetState messages and then continue
mbox.TryScan(fun m ->
match m with
| GetState(chnl) ->
printfn "G processing TryScan"
chnl.Reply(state)
Some(async { return! loop state})
| _ -> None
) |> ignore
let! msg = mbox.Receive()
match msg with
| UpdateState ->
printfn "U processing"
// something very time consuming here...
async { do! Async.Sleep(1000) } |> Async.RunSynchronously
return! loop (state+1)
| GetState(chnl) ->
printfn "G processing"
chnl.Reply(state)
return! loop state
}
loop 0
)
[async { for i in 1..10 do
printfn " U"
mbox.Post(UpdateState)
async { do! Async.Sleep(200) } |> Async.RunSynchronously
};
async { // wait some time so that several `UpdateState` messages are fired
async { do! Async.Sleep(500) } |> Async.RunSynchronously
for i in 1..20 do
printfn "G"
printfn "%d" (mbox.PostAndReply(GetState))
}] |> Async.Parallel |> Async.RunSynchronously
如果您嘗試運行代碼,您將看到, GetState
消息幾乎不會被處理,因為它等待結果。 另一方面, UpdateState
只是偶然發生,因此阻止了有效獲取狀態。
編輯
目前適用於我的解決方案是這樣的:
type Msg = GetState of AsyncReplyChannel<int> | UpdateState
let mbox = MailboxProcessor.Start(fun mbox ->
let rec loop state = async {
// this TryScan doesn't work as expected
// it should process GetState messages and then continue
let! res = mbox.TryScan((function
| GetState(chnl) -> Some(async {
chnl.Reply(state)
return state
})
| _ -> None
), 5)
match res with
| None ->
let! msg = mbox.Receive()
match msg with
| UpdateState ->
async { do! Async.Sleep(1000) } |> Async.RunSynchronously
return! loop (state+1)
| _ -> return! loop state
| Some n -> return! loop n
}
loop 0
)
對注釋的反應:與並行執行UpdateState
其他MailboxProcessor
或ThreadPool
的想法很棒,但我目前不需要它。 我想做的就是處理所有GetState
消息,然后處理其他消息。 我不關心在處理UpdateState
期間代理是否被阻止。
我會告訴你輸出的問題是什么:
// GetState messages are delayed 500 ms - see do! Async.Sleep(500)
// each UpdateState is sent after 200ms
// each GetState is sent immediatelly! (not real example, but illustrates the problem)
U 200ms <-- issue UpdateState
U processing <-- process UpdateState, it takes 1sec, so other
U 200ms 5 requests are sent; sent means, that it is
U 200ms fire-and-forget message - it doesn't wait for any result
and therefore it can send every 200ms one UpdateState message
G <-- first GetState sent, but waiting for reply - so all
previous UpdateState messages have to be processed! = 3 seconds
and AFTER all the UpdateState messages are processed, result
is returned and new GetState can be sent.
U 200ms
U 200ms because each UpdateState takes 1 second
U 200ms
U processing
U
U
U
U
U processing
G processing <-- now first GetState is processed! so late? uh..
U processing <-- takes 1sec
3
G
U processing <-- takes 1sec
U processing <-- takes 1sec
U processing <-- takes 1sec
U processing <-- takes 1sec
U processing <-- takes 1sec
U processing <-- takes 1sec
G processing <-- after MANY seconds, second GetState is processed!
10
G
G processing
// from this line, only GetState are issued and processed, because
// there is no UpdateState message in the queue, neither it is sent
我不認為TryScan
方法會在這種情況下幫助您。 它允許您指定在等待消息時使用的超時。 收到某條消息后,它將開始處理該消息(忽略超時)。
例如,如果您想等待某些特定消息,但每秒執行一些其他檢查(等待時),您可以寫:
let loop () = async {
let! res = mbox.TryScan(function
| ImportantMessage -> Some(async {
// process message
return 0
})
| _ -> None)
match res with
| None ->
// perform some check & continue waiting
return! loop ()
| Some n ->
// ImportantMessage was received and processed
}
在處理UpdateState
消息時,您可以做些什么來避免阻止郵箱處理器? 郵箱處理器(邏輯上)是單線程的 - 您可能不希望取消UpdateState
消息的處理,因此最好的選擇是在后台開始處理它並等待處理完成。 然后,處理UpdateState
的代碼可以將一些消息發送回郵箱(例如UpdateStateCompleted
)。
以下是草圖的外觀:
let rec loop (state) = async {
let! msg = mbox.Receive()
match msg with
| GetState(repl) ->
repl.Reply(state)
return! scanning state
| UpdateState ->
async {
// complex calculation (runs in parallel)
mbox.Post(UpdateStateCompleted newState) }
|> Async.Start
| UpdateStateCompleted newState ->
// Received new state from background workflow
return! loop newState }
現在后台任務並行運行,您需要注意可變狀態。 此外,如果您發送UpdateState
消息的速度超過了處理它們的速度,那么您將遇到麻煩。 例如,當您處理前一個請求時忽略或排隊請求,可以修復此問題。
不要使用TRYSCAN!
不幸的是,當前版本的F#中的TryScan
功能有兩種方式。 首先,重點是指定超時,但實現實際上並不尊重它。 具體而言,不相關的消息會重置計時器。 其次,與其他Scan
功能一樣,在鎖定下檢查消息隊列,該鎖定防止任何其他線程在掃描期間發布,這可能是任意長的時間。 因此, TryScan
函數本身往往會鎖定並發系統,甚至可能會引入死鎖,因為調用者的代碼是在鎖內部進行評估的(例如,當鎖定下的代碼阻塞時,從函數參數發送到Scan
或TryScan
會使代理死鎖獲得已經存在的鎖定。
我在生產代碼的早期原型中使用了TryScan
,它沒有引起任何問題。 但是,我設法圍繞它進行構建,結果架構實際上更好。 本質上,我急切地Receive
所有消息並使用我自己的本地隊列進行過濾。
正如Tomas所說,MailboxProcessor是單線程的。 您將需要另一個MailboxProcessor來在狀態getter的單獨線程上運行更新。
#nowarn "40"
type Msg =
| GetState of AsyncReplyChannel<int>
| UpdateState
let runner_UpdateState = MailboxProcessor.Start(fun mbox ->
let rec loop = async {
let! state = mbox.Receive()
printfn "U start processing %d" !state
// something very time consuming here...
do! Async.Sleep 100
printfn "U done processing %d" !state
state := !state + 1
do! loop
}
loop
)
let mbox = MailboxProcessor.Start(fun mbox ->
// we need a mutiple state if another thread can change it at any time
let state = ref 0
let rec loop = async {
let! msg = mbox.Receive()
match msg with
| UpdateState -> runner_UpdateState.Post state
| GetState chnl -> chnl.Reply !state
return! loop
}
loop)
[
async {
for i in 1..10 do
mbox.Post UpdateState
do! Async.Sleep 200
};
async {
// wait some time so that several `UpdateState` messages are fired
do! Async.Sleep 1000
for i in 1..20 do
printfn "G %d" (mbox.PostAndReply GetState)
do! Async.Sleep 50
}
]
|> Async.Parallel
|> Async.RunSynchronously
|> ignore
System.Console.ReadLine() |> ignore
輸出:
U start processing 0
U done processing 0
U start processing 1
U done processing 1
U start processing 2
U done processing 2
U start processing 3
U done processing 3
U start processing 4
U done processing 4
G 5
U start processing 5
G 5
U done processing 5
G 5
G 6
U start processing 6
G 6
G 6
U done processing 6
G 7
U start processing 7
G 7
G 7
U done processing 7
G 8
G U start processing 8
8
G 8
U done processing 8
G 9
G 9
U start processing 9
G 9
U done processing 9
G 9
G 10
G 10
G 10
G 10
您也可以使用ThreadPool。
open System.Threading
type Msg =
| GetState of AsyncReplyChannel<int>
| SetState of int
| UpdateState
let mbox = MailboxProcessor.Start(fun mbox ->
let rec loop state = async {
let! msg = mbox.Receive()
match msg with
| UpdateState ->
ThreadPool.QueueUserWorkItem((fun obj ->
let state = obj :?> int
printfn "U start processing %d" state
Async.Sleep 100 |> Async.RunSynchronously
printfn "U done processing %d" state
mbox.Post(SetState(state + 1))
), state)
|> ignore
| GetState chnl ->
chnl.Reply state
| SetState newState ->
return! loop newState
return! loop state
}
loop 0)
[
async {
for i in 1..10 do
mbox.Post UpdateState
do! Async.Sleep 200
};
async {
// wait some time so that several `UpdateState` messages are fired
do! Async.Sleep 1000
for i in 1..20 do
printfn "G %d" (mbox.PostAndReply GetState)
do! Async.Sleep 50
}
]
|> Async.Parallel
|> Async.RunSynchronously
|> ignore
System.Console.ReadLine()|>忽略
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.