The following test shows that Async.Sleep in F# 2.0 can not be canceled immediately. We will get 'cancel' notification only after the time will elapsed.
module async_sleep_test
open System
open System.Threading
open System.Threading.Tasks
open System.Xml
let cts = new CancellationTokenSource()
Task.Factory.StartNew(fun () ->
try
Async.RunSynchronously(async{
printfn "going to sleep"
do! Async.Sleep(10000)
}, -1(*no timeout*), cts.Token)
printfn "sleep completed"
with
| :? OperationCanceledException ->
printfn "sleep aborted" // we will see it only after 10 sec.
| _ ->
printfn "sleep raised error"
) |> ignore
Thread.Sleep(100) // give time to the task to enter in sleep
cts.Cancel()
Thread.Sleep(100) // give chance to the task to complete before print bye message
printfn "press any key to exit...."
Console.ReadKey(true) |> ignore
I think this is incorrect behavior. How do you think is this a bug? Will be there any surprises if for example I will use the following implementation:
static member SleepEx(milliseconds:int) = async{
let disp = new SerialDisposable()
use! ch = Async.OnCancel(fun()->disp.Dispose())
do! Async.FromContinuations(fun (success, error, cancel) ->
let timerSubscription = new SerialDisposable()
let CompleteWith =
let completed = ref 0
fun cont ->
if Interlocked.Exchange(completed, 1) = 0 then
timerSubscription.Dispose()
try cont() with _->()
disp.Disposable <- Disposable.Create(fun()->
CompleteWith (fun ()-> cancel(new OperationCanceledException()))
)
let tmr = new Timer(
callback = (fun state -> CompleteWith(success)),
state = null, dueTime = milliseconds, period = Timeout.Infinite
)
if tmr = null then
CompleteWith(fun ()->error(new Exception("failed to create timer")))
else
timerSubscription.Disposable <- Disposable.Create(fun()->
try tmr.Dispose() with _ -> ()
)
)
}
sleep will block a thread and task. delay will not and has a cancellation token, unless your app is pretty complex, it really doesn't matter as on the surface: task. delay and thread. sleep do pretty much the same thing.
It's fully async. And on top of that, virtual threads are super cheap. You could create hundreds of thousands of them without overhead or limitations.
I wouldn't say this is a bug - it follows from the way cancellation is handled in F# asynchronous workflows in general. Generally, F# assumes that primitive operations that you call using let!
or do!
do not support cancellation (I guess there is no standard mechanism for that in .NET), so F# inserts cancellation checks before and after the call made using let!
.
So a call let! res = foo()
is actually more like the following (although the checks are hidden in the library implementation of async
):
token.ThrowIfCancellationRequested()
let! res = foo()
token.ThrowIfCancellationRequested()
Of course, the workflow returned by foo()
can handle cancellation better - typically, if it is implemented using an async { .. }
block, then it will contain more checks around each let!
. However, in general (unless some operation is implemented in a more clever way), the cancellation will be performed after the next let!
call completes.
Your alternative definition of Sleep
looks pretty good to me - it supports cancellation better than the one available in F# library and if you need immediate cancellation, then replacing F#'s Async.Sleep
with your SleepEx
is the only way to go. However, there are probably still going to be some operations that do not support immmediate cancellation, so you may run into problems elsewhere (if you need this behavior everywhere).
PS: I think your SleepEx
function could be pretty useful for others. If you could share it on the F# Snippets web site, that would be fantastic!
I think this is incorrect behavior. How do you think is this a bug?
Yes, I thought it was a bug. I reported it as a bug. Microsoft agreed it was a bug. They've fixed the bug in F# 3.0 / VS2012 along with bugs in TryScan
and others.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With