Last time we looked at the Async class, and examined some of its core functions. This time we will be looking at using some Task Parallel Library (TPL) classes, namely Task<T>
, and Task. We will also examine how the Async module can be used in conjunction with TPL.
TPL Primer
I do not have enough time in this post to go through all the nitty gritty details of TPL, but I will just mention a few key points
- TPL uses a
Task<T>
to represent a asynchronous operation that will return a value T in this case (yes a generic so anything your heart desires) - TPL uses a Task to represent a asynchronous operation that doesn’t return a value. Unit in F# lingo
- In TPL there are several trigger values that cause the
Task<T>
to be observed. Things like Wait / WaitAll / Result will also cause the tasks to be observed. These are however blocking operations that suspend the calling thread. - TPL may also use
CancellationTokens
to cancel async operations (albeit you need a bit more code in C# than you do in F# due to the fact that in C# you must constantly check the CancellationToken
, which we saw in the previous post) - Both
Task<T>
and Task can be waited on - Both
Task<T>
and Task can run things known as continuations, which are essentially callbacks when the Task<T>
/ Task is done. You may schedule callback for when a Task ran to completion, or is faulted, or both, or none Task<T>
and Task for the basis of the new async/await syntax in C#
Starting And Waiting For Task<T>
In this simple example we will show how to create a simple Task<T>
that returns a boolean. We will the use the blocking Task<T>.Wait()
method, to obtain the result of the Task<T>
, which will be a boolean in this case.
open System
open System.Threading
open System.Threading.Tasks
[<EntryPoint>]
let main argv =
let work() =
for i in 0 .. 2 do
printfn "Work loop is currently %O" i |> ignore
Thread.Sleep(1000)
false
printfn "Starting task that returns a value" |> ignore
let task = Task.Factory.StartNew<bool>((fun () -> work()), TaskCreationOptions.LongRunning)
let result = task.Result
printfn "Task result is %O" result
Console.ReadLine() |> ignore
0
Which when run gives the following output
We could also do this another way too which would yield the same results. We could use a continuation from the original Task<T>
that is run when the original task runs to completion. Think of continuations as callbacks. Here is the code rewritten to use a continuation, remember you can have a single callback for the whole original task, or hook up specific ones for particular scenarios, which is what I have done here.
open System
open System.Threading
open System.Threading.Tasks
[<EntryPoint>]
let main argv =
let work() =
for i in 0 .. 2 do
printfn "Work loop is currently %O" i |> ignore
Thread.Sleep(1000)
false
printfn "Starting task that returns a value" |> ignore
let task = Task.Factory.StartNew<bool>((fun () -> work()), TaskCreationOptions.LongRunning)
task.ContinueWith((fun (antecedant : Task<bool>) -> printfn "Task result is %O" antecedant.Result),
TaskContinuationOptions.OnlyOnRanToCompletion) |> ignore
Console.ReadLine() |> ignore
0
Starting And Waiting For Task<T> In A More F# Like Way
The Async
class offers a couple of helpers when dealing with tasks, you may use
Async.StartAsTask
Async.AwaitTask
Here is some code that shows how you can use these
open System
open System.Threading
open System.Threading.Tasks
[<EntryPoint>]
let main argv =
let work = async {
for i in 0 .. 2 do
printfn "Work loop is currently %O" i |> ignore
do! Async.Sleep(1000)
return "task is completed " + DateTime.Now.ToLongTimeString()
}
printfn "Starting task that returns a value" |> ignore
let asynWorkflow = async {
let task = Async.StartAsTask((work))
let! result = Async.AwaitTask(task)
return result
}
let finalResult = Async.RunSynchronously asynWorkflow
printfn "Task result is : %O" finalResult
Console.ReadLine() |> ignore
0
Here are the results of running the above code:
Starting And Waiting For Plain Task
Another thing you might find yourself wanting to do is a use a TPL Task
. That is a Task
that does not return a value, basically you have Task<T>
which is a task that returns T, and Task
(essentially Task
void, or Task<Unit>
in F# lingo), which is a task that doesn’t return a value. Task may still be waited on in C# land, but there seems to be less you can do with a standard Task (one that doesn’t return a value) in F#.
There however a few tricks you can do, the first one requires a bit of insight into multi threading anyway, which is that Task
, and Task<T>
for that matter both implement IAsyncResult
, which is something you can wait on inside of a F# async workflow, by using Async.AwaitIAsyncResult
. Here is a small example, of how you can wait on a plain Task. This example also demonstrates how you can extend the Async module to include your own user specified functions. That is pretty cool actually, C# allows extension methods (which F# also allows), but being able to just add arbitrary functions is very cool.
Anyway here is the code:
open System
open System.Threading
open System.Threading.Tasks
module Async =
let AwaitVoidTask : (Task -> Async<unit>) =
Async.AwaitIAsyncResult >> Async.Ignore
[<EntryPoint>]
let main argv =
let theWorkflow(delay :int) = async {
printfn "Starting workflow at %O" (DateTime.Now.ToLongTimeString())
do! Task.Delay(delay) |> Async.AwaitVoidTask
printfn "Ending workflow at %O" (DateTime.Now.ToLongTimeString())
}
Async.RunSynchronously (theWorkflow(2000))
Console.ReadLine() |> ignore
0
Which when run gives the following result:
Some other clever chap who maintains this blog https://gist.github.com/theburningmonk/3921623 has a slightly different take on this. Here is his version, which I also think has many merits, for example it is really nice that it will pattern match against a Faulted Task and raise an Exception
open System
open System.Threading
open System.Threading.Tasks
module Async =
let inline awaitPlainTask (task: Task) =
let continuation (t : Task) : unit =
match t.IsFaulted with
| true -> raise t.Exception
| arg -> ()
task.ContinueWith continuation |> Async.AwaitTask
let inline startAsPlainTask (work : Async<unit>) =
Task.Factory.StartNew(fun () -> work |> Async.RunSynchronously)
[<EntryPoint>]
let main argv =
let sleepy = async {
do! Async.Sleep(5000)
printfn "awake"
}
let sleepy2 = async {
do! sleepy |> Async.startAsPlainTask |> Async.awaitPlainTask
printfn "feeling sleepy again…"
}
sleepy |> Async.startAsPlainTask |> ignore
sleepy2 |> Async.Start |> ignore
Console.ReadLine() |> ignore
0
Which gives the following results when run:
Starting And Waiting For Multiple Tasks
To wait for multiple Task<T>
you can use TPLs Task.WhenAll()
for this, which will give you an aggregated result task, which will have a result object which contains the results from the original tasks you used in the Task.WaitAll()
call.
There may well be a way that you can bend the Async.Parallel()
to do the same job, but to my mind using Task.WhenAll()
is by far the easiest way.
Here is some code that demonstrates this
open System
open System.Threading
open System.Threading.Tasks
[<EntryPoint>]
let main argv =
let work(msg) =
for i in 0 .. 2 do
printfn "%O : Work loop is currently %O\r\n" msg i |> ignore
Thread.Sleep(1000)
"Task 1 done " + (DateTime.Now.ToLongTimeString())
let taskRunner(msg) =
printfn "Starting %O that returns a value %O\r\n" msg (DateTime.Now.ToLongTimeString()) |> ignore
Task.Factory.StartNew<string>((fun () -> work(msg)), TaskCreationOptions.LongRunning)
let task1 = taskRunner("task1")
Thread.Sleep(2000)
let task2 = taskRunner("task2")
Thread.Sleep(2000)
let task3 = taskRunner("task3")
let resultsOfAllTask = Task.WhenAll([task1;task2;task3])
printfn "Task1 result: %O\r\nTask2 result: %O\r\nTask3 result: %O"
resultsOfAllTask.Result.[0]
resultsOfAllTask.Result.[1]
resultsOfAllTask.Result.[2]
Console.ReadLine() |> ignore
0
Which when run will give the following results