Skip to content

Commit

Permalink
Fix 581 - Async.RunSynchronously tries to reuse current thread
Browse files Browse the repository at this point in the history
  • Loading branch information
radekm committed Dec 5, 2015
1 parent bbb086e commit c8b3c56
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ type AsyncType() =

()

[<Test>]
member this.AsyncRunSynchronouslyReusesThreadPoolThread() =
let action = async { async { () } |> Async.RunSynchronously }
let computation =
[| for i in 1 .. 1000 -> action |]
|> Async.Parallel
// This test needs approximately 1000 ThreadPool threads
// if Async.RunSynchronously doesn't reuse them.
// In such case TimeoutException is raised
// since ThreadPool cannot provide 1000 threads in 1 second
// (the number of threads in ThreadPool is adjusted slowly).
Assert.DoesNotThrow(fun () ->
Async.RunSynchronously(computation, timeout = 1000)
|> ignore)

[<Test>]
member this.AsyncSleepCancellation1() =
ignoreSynchCtx (fun () ->
Expand Down
36 changes: 35 additions & 1 deletion src/fsharp/FSharp.Core/control.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1226,7 +1226,7 @@ namespace Microsoft.FSharp.Control

module CancellationTokenOps =
/// Run the asynchronous workflow and wait for its result.
let RunSynchronously (token:CancellationToken,computation,timeout) =
let private RunSynchronouslyInAnotherThread (token:CancellationToken,computation,timeout) =
let token,innerCTS =
// If timeout is provided, we govern the async by our own CTS, to cancel
// when execution times out. Otherwise, the user-supplied token governs the async.
Expand Down Expand Up @@ -1261,6 +1261,40 @@ namespace Microsoft.FSharp.Control
| None -> ()
commit res

let private RunSynchronouslyInCurrentThread (token:CancellationToken,computation) =
use resultCell = new ResultCell<Result<_>>()
let trampolineHolder = TrampolineHolder()

trampolineHolder.Protect
(fun () ->
startA
token
trampolineHolder
(fun res -> resultCell.RegisterResult(Ok(res),reuseThread=true))
(fun edi -> resultCell.RegisterResult(Error(edi),reuseThread=true))
(fun exn -> resultCell.RegisterResult(Canceled(exn),reuseThread=true))
computation)
|> unfake

commit (resultCell.TryWaitForResultSynchronously() |> Option.get)

let RunSynchronously (token:CancellationToken,computation,timeout) =
// Reuse the current ThreadPool thread if possible. Unfortunately
// Thread.IsThreadPoolThread isn't available on all profiles so
// we approximate it by testing synchronization context for null.
match SynchronizationContext.Current, timeout with
| null, None -> RunSynchronouslyInCurrentThread (token, computation)
// When the timeout is given we need a dedicated thread
// which cancels the computation.
// Performing the cancellation in the ThreadPool eg. by using
// Timer from System.Threading or CancellationTokenSource.CancelAfter
// (which internally uses Timer) won't work properly
// when the ThreadPool is busy.
//
// And so when the timeout is given we always use the current thread
// for the cancellation and run the computation in another thread.
| _ -> RunSynchronouslyInAnotherThread (token, computation, timeout)

let Start (token:CancellationToken,computation) =
queueAsync
token
Expand Down

0 comments on commit c8b3c56

Please sign in to comment.