dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.93k stars 787 forks source link

MailboxProcessor strange behavior #18045

Open flarelee opened 3 days ago

flarelee commented 3 days ago

Please provide a succinct description of the issue.

MailboxProcessor agent did not receive message and run forever!

Expected behavior

After dispose agent, agent should stop.

Actual behavior

agent run forever.

Known workarounds

add some sleep time, it may be work.

Related information

Provide any related information (optional):

// print text to console
let printText = 
    let agent = MailboxProcessor.Start(fun inbox -> async {
        while true do
            let! msg = inbox.Receive()
            printfn "%s: %s" (System.DateTime.Now.ToLongTimeString()) msg

    })
    agent.Post

let teskMailboxLeak(withSleep) =
    use agent = new MailboxProcessor<int>(fun inbox -> async {
        let mutable i = 0
        while i >= 0 do
            let! (msg: int option) = inbox.TryReceive(2000)

            match msg with
            | None ->  
                sprintf "idle %d" i |> printText
                i <- i + 1
            | Some x -> 
                sprintf "receive %d" x |> printText

            let! cts = Async.CancellationToken
            if cts.IsCancellationRequested then 
                i <- -1
        printText "exit"
    })

    agent.Start()
    agent.Post(3) 
    agent.Post(4)
    agent.Post(5)

    if withSleep then
        System.Threading.Thread.Sleep(4000)
    //! with sleep, everything works as expected.

    //! without sleep
    //! the agent run forever! 
    //! f# 4.7.2 can receive numbers, f# 8 or 9 can not!
    //! Is it normal?

[<EntryPoint>]
let main argv =
    let withSleep = false
    printText ("start " + if withSleep then "with sleep" else "without sleep")
    teskMailboxLeak(withSleep)

    //System.Threading.Thread.Sleep(5000)
    // force GC
    for i = 1 to 5 do
        sprintf "GC %d" i |> printText
        System.GC.Collect(System.GC.MaxGeneration) |> ignore
        System.GC.WaitForFullGCComplete() |> ignore
        System.Threading.Thread.Sleep(1000)

    System.Console.ReadLine() |> ignore
    0 // return an integer exit code

log without sleep

08:50:48: start without sleep
08:50:48: GC 1
08:50:49: GC 2
08:50:50: idle 0
08:50:50: GC 3
08:50:51: GC 4
08:50:52: idle 1
08:50:52: GC 5
08:50:54: idle 2
08:50:56: idle 3
08:50:58: idle 4
08:51:00: idle 5
08:51:02: idle 6
08:51:04: idle 7
08:51:06: idle 8
08:51:08: idle 9
08:51:10: idle 10
08:51:12: idle 11
08:51:14: idle 12
08:51:16: idle 13
08:51:18: idle 14
08:51:20: idle 15
08:51:23: idle 16
08:51:25: idle 17
08:51:27: idle 18
08:51:29: idle 19
...

log with sleep

09:18:30: start with sleep
09:18:30: receive 3
09:18:30: receive 4
09:18:30: receive 5
09:18:32: idle 0
09:18:34: GC 1
09:18:34: idle 1
09:18:35: GC 2
09:18:36: GC 3
09:18:37: GC 4
09:18:38: GC 5
majocha commented 2 days ago

Yes. this is counterintuitive. MailboxProcessor does not hold any internal CancellationTokenSource that it can cancel on Dispose. Internally it just does a Async.Start. I guess if there is no stopping condition in the body, it will run even after disposal, or wait forever on a disposed handle.

We could probably add cancellation on Dispose utilizing a linked token source. Would it be a breaking change?

Martin521 commented 2 days ago

6285, #17849, #11282