Reputation: 666
Consider the following code:
open System
open System.Diagnostics
open System.Threading
open System.Threading.Tasks
type Async with
static member WithTimeout (timeout: int) operation =
async {
let! child = Async.StartChild (operation, timeout)
try
let! _result = child
return true
with :? TimeoutException -> return false
}
static member WithTaskTimeout<'T> (timeout: int) (operation: Async<'T>) = async {
let delay = Task.Delay(timeout)
let! task = Task.WhenAny(operation |> Async.StartAsTask :> Task, delay) |> Async.AwaitTask
if task = delay then
return false
else
return true
}
[<EntryPoint>]
let main _ =
let syncSleep = async {
Thread.Sleep(4000)
return 1
}
let asyncSleep = async {
do! Async.Sleep(4000)
return 1
}
let run name async =
let time action prefix =
let sw = Stopwatch.StartNew()
let result = action |> Async.RunSynchronously
sw.Stop()
printfn "%s | %s returned %O. Elapsed: %O" prefix name result sw.Elapsed
time (async |> Async.WithTimeout 2000) "Async"
time (async |> Async.WithTaskTimeout 2000) "Task "
run "Thread.Sleep" syncSleep
run "Async.Sleep " asyncSleep
0
On Mono 5.18.1.3 it produces the following output:
Async | Thread.Sleep returned False. Elapsed: 00:00:04
Task | Thread.Sleep returned False. Elapsed: 00:00:02
Async | Async.Sleep returned False. Elapsed: 00:00:02
Task | Async.Sleep returned False. Elapsed: 00:00:02
So when child async has synchronous wait inside, Async.StartChild
returns not when timeout passes, but when the inner async comes to a completion.
At the same time task-based execute with timeout in both calls returns just only after timeout.
Why Async.StartChild
behaves this way?
Upvotes: 2
Views: 218
Reputation: 13577
The way timeouts are handled in async workflows is through cancellations. What happens in your scenario with a blocking wait is that the first possible moment to check for the cancellation is after the wait completes.
Async workflows use a model called cooperative cancellation, which means that the caller and the callee cooperate on handling the cancellation through an intermediary CancellationToken
. When Async.StartChild
needs to cancel the child workflow, it will request a cancellation on the token, and then it falls to the child workflow to check the state of the cancellation token and call the cancellation continuation. Those checks are baked into async primitives, look for IsCancellationRequested
here.
Since your child workflow is blocked on Thread.Sleep
, this wil not happen until the sleep completes.
Worth noting that the same model is used by TPL tasks. You just don't see it because your task timeout depends on the semantics of Task.WhenAny
- and those might not be what you expect w.r.t. the remaining running tasks.
Upvotes: 1