Skip to content

Commit

Permalink
feat/async_v2: revamping the Asyncronnous Code section
Browse files Browse the repository at this point in the history
  • Loading branch information
⚙︎ Greg committed Jul 9, 2024
1 parent 4b8dc88 commit adbbc5c
Showing 1 changed file with 40 additions and 28 deletions.
68 changes: 40 additions & 28 deletions docs/fsharp-cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,6 @@ Another way of implementing interfaces is to use *object expressions*.
| DivisibleBy 5 -> "Buzz"
| i -> string i


<div id="asynchronous-programming"></div>

## Asynchronous Programming
Expand All @@ -758,67 +757,76 @@ In F#, .NET Tasks can be constructed using the `task { }` computational expressi
open System.Threading.Tasks
open System.IO

let readFileTask filename ct = task {
let readFile filename ct = task {
printfn "Started Reading Task"
do! Task.Delay((TimeSpan.FromSeconds 5), cancellationToken = ct)
let! text = File.ReadAllTextAsync(filename, ct)
do! Task.Delay((TimeSpan.FromSeconds 5), cancellationToken = ct) // use do! when awaiting a Task
let! text = File.ReadAllTextAsync(filename, ct) // use let! when awaiting a Task<'T>, and unwrap 'T from Task<'T>.
printfn "Finished Reading Task"
return text
}
//gwh - one task to clarify non-repeatable
let readFileTask: Task<string> = readFile "myfile.txt" CancellationToken.None // (before return) Output: Started Reading Task

let task1: Task<string> = readFileTask "A" CancellationToken.None // Output: Started Reading Task
let task2: Task<string> = readFileTask "B" CancellationToken.None // Output: Started Reading Task

let textOfFileA = task1.Result // Output: Finished Reading Text
let textOfFileB = task2.Result // Output: Finished Reading Text
// (readFileTask continues execution on the ThreadPool)

.NET Tasks are the central component of the task-based asynchronous pattern in .NET. Because of this, F# has `Async.AwaitTask` that for easier interop with asynchronous workflows.
let fileContent = readFileTask.Result // (before returning) Output: Finished Reading Text
let fileContent' = readFileTask.Result // Task is already completed, returns same text; no output

### Asynchronous Workflows

Asynchronous workflows were invented before .NET Tasks existed, which is why F# has two core methods for asynchronous programming. However, Asynchronous Workflows did not become obsolete. They offer another, but different, approach: dataflow.
Asynchronous blocks are constructed using the `async { }` expression. Then using the [`Async` library](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html#section3) the blocks are composed and executed.
In contrast to .NET Tasks, asynchronous workflows are "cold" - need to be explicitly started - and every workflow passes a CancellationToken implicitly.
Asynchronous blocks are constructed using the `async { }` expression. Then using the [`Async` module](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html#section3) the blocks are composed and executed.
In contrast to .NET Tasks, asynchronous workflows are "cold" (need to be explicitly started) and every workflow passes a CancellationToken implicitly.

open System
open System.Threading
open System.IO

let readFile filename = async {
printfn "Started Reading Async"
let! cancellationToken = Async.CancellationToken // implicit token
let! text = File.ReadAllTextAsync(filename, cancellationToken) |> Async.AwaitTask
// gwh - added some comments
do! Async.Sleep(TimeSpan.FromSeconds 5) // use do! when awaiting an Async
let! cancellationToken = Async.CancellationToken // (1)
let! text = File.ReadAllTextAsync(filename, cancellationToken) |> Async.AwaitTask // (2)
printfn "Finished Reading Async"
return text
}

// compose an async workflow from exising async computations
let readFilesWorkflow = [ readFile "A"; readFile "B" ] |> Async.Parallel

//gwh - added a token
// run async workflow
let textOfFiles: string[] = readFilesWorkflow |> Async.RunSynchronously
let textOfFiles: string[] = readFilesWorkflow |> Async.RunSynchronously (CancellationToken.None)
// Output: Started Reading Async
// Output: Started Reading Async
// Output: Finished Reading Async
// Output: Finished Reading Async

// run async workflow again; repeatable
let textOfFiles': string[] = readFilesWorkflow |> Async.RunSynchronously
//gwh - removed "repeatable" and modified the Task example
// run async workflow again
let textOfFiles': string[] = readFilesWorkflow |> Async.RunSynchronously (CancellationToken.None)
// Output: Started Reading Async
// Output: Started Reading Async
// Output: Finished Reading Async
// Output: Finished Reading Async

(1) See [Async Cancellation](#asynchronous-programming-cancellation-async)
gwh Moved this here and reworded slightly. i took my choice of wording from: https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-threading-tasks-task
(2) As .NET Tasks became the central component of all the task-based asynchronous pattern after Asynchronous Workflows were introduced, F#'s Async has `Async.AwaitTask` for easy interop.

#### Creation / Composition

The `Async` library has a number of functions to compose and start workflows. The full list with explanations can be found in the [Async Type Reference](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html#section0).
//gwh library -> module

| Function | Description |
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Async.Ignore | Creates an `Async<unit>` workflow from an `Async<'T>` |
| Async.Parallel | Composes a new workflow from multiple workflows, `Async<'T> seq`, and runs them in parallel; it returns all the results in an array `Async<'T array>` |
| Async.Sequential | Composes a new workflow from multiple workflows, `Async<'T> seq`, and runs them in series; it returns all the results in an array `Async<'T array>` |
| Async.Choice | Composes a new workflow from multiple workflows, `Async<'T option> seq`, and returns the first where `'T'` is `Some value` (all others running are canceled). If all workflows return `None` then the result is `None` |
The `Async` module has a number of functions to compose and start workflows. The full list with explanations can be found in the [Async Type Reference](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html#section0).

| Function | Description |
|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Async.Ignore | Creates an `Async<unit>` workflow from an `Async<'T>` |
| Async.Parallel | Composes a new workflow from multiple workflows, `Async<'T> seq`, and runs them in parallel; it returns all the results in an array `Async<'T array>` |
| Async.Sequential | Composes a new workflow from multiple workflows, `Async<'T> seq`, and runs them in series; it returns all the results in an array `Async<'T array>` |
| Async.Choice | Composes a new workflow from multiple workflows, `Async<'T option> seq`, and returns the first where `'T'` is `Some value` (all others running are canceled). If all workflows return `None` then the result is `None` |

For all functions that compose a new workflow from children, if any child computations raises an exception, then the overall computation will trigger an exception, and cancel the others.
If canceled, the computation will cancel any remaining child computations but will still wait for the other child computations to complete.
Expand Down Expand Up @@ -846,25 +854,29 @@ If canceled, the computation will cancel any remaining child computations but wi
for cnt in [ 0 .. 10 ] do
token.ThrowIfCancelled()
printf $"{cnt}: And..."
do! Task.Delay ((TimeSpan.FromSeconds 1), token) // token is required here; otherwise "Done" will be printed before cancelling
//gwh
do! Task.Delay ((TimeSpan.FromSeconds 1), token) // token is required for Task.Delay to be interrupted by CancellationToken
printfn "Done"
}

let cts = new CancellationTokenSource (TimeSpan.FromSeconds 4)
let runningLoop = loop cts.Token
runningLoop.Wait()

<div id="asynchronous-programming-cancellation-async"></div>

#### Async

Asynchronous workflows have the benefit of having an implicit Cancellation Token. You can provide one that will propagate amongst all children in the workflow,
or one is created for you; accessed via `Async.DefaultCancellationToken`
// gwh removed mention of DefaultToken

Asynchronous workflows have the benefit of implicit Cancellation Token passing and checking

open System
open System.Threading
let loop = async {
for cnt in [ 1 .. 10 ] do
printf $"{cnt}: And..."
do! Async.Sleep (TimeSpan.FromSeconds 1) // Async.Sleep implicitly receives the Cancellation Token
do! Async.Sleep (TimeSpan.FromSeconds 1) // Async.Sleep implicitly receives and checks `cts.Token`

let! ct = Async.CancellationToken // when interoping with Tasks, cancellationTokens need to be passed explicitly
do! Task.Delay ((TimeSpan.FromSecond 1), cancellationToken = ct) |> Async.AwaitTask
Expand Down

0 comments on commit adbbc5c

Please sign in to comment.