-
Notifications
You must be signed in to change notification settings - Fork 842
Description
The current implementation of the use construct of the asynchronous workflow expressions has a race condition with respect to cancellation: if cancellation is requested at the right time, a disposable may be constructed, but dispose will not be called.
Consider the implementation of use in terms of try-finally. Due to cancellation, the implementation of try-finally does not translate to an implementation of use that guarantees disposal.
Here is a pair of examples that demonstrates the issue:
open System
open System.Threading
do let source = new CancellationTokenSource ()
let token = source.Token
let example = async {
printfn "A: Here we go"
use! h = Async.OnCancel <| fun () -> printfn "A: We got cancelled"
use x = source.Cancel ()
printfn "A: Creating disposable"
{new IDisposable with
override this.Dispose () = printfn "A: Disposed properly"}
do printfn "A: Never getting here"
}
Async.Start (example, token)
do let source = new CancellationTokenSource ()
let token = source.Token
let example = async {
printfn "B: Here we go"
use! h = Async.OnCancel <| fun () -> printfn "B: We got cancelled"
use x = printfn "B: Creating disposable"
{new IDisposable with
override this.Dispose () = printfn "B: Disposed properly"}
do source.Cancel ()
do! async { return () }
do printfn "B: Never getting here"
}
Async.Start (example, token)Here is sample output obtained by running the above with F# Interactive for F# 4.0 (Open Source Edition):
B: Here we go
A: Here we go
B: Creating disposable
A: We got cancelled
B: We got cancelled
A: Creating disposable
B: Disposed properly
As can be seen, in both cases disposables are created, but only in case B the disposable is disposed. Note that the example uses synchronous code to trigger the behavior. In actual use scenarios cancellation is triggered by code on other threads and the result is that disposal will be non-deterministically cancelled.
I would expect use to guarantee disposal in both cases.