question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Binding to `Async<'T>` in a `task` CE uses StartAsTask, which involves a context switch

See original GitHub issue

This was pointed out to me by @bartelink while working on TaskSeq, separately reported in this issue: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/135, where I took the same approach as task.

The implementation for binding to an Async<'T> in the task computation expression builder is as follows:

member inline this.Bind(computation: Async<'TResult1>, continuation: ('TResult1 -> TaskCode<'TOverall, 'TResult2>)) : TaskCode<'TOverall, 'TResult2> =
    this.Bind(Async.StartAsTask computation, continuation)

Per the discussion here, its points by @gusty, and specifically this answer (https://github.com/dotnet/fsharp/discussions/11043#discussioncomment-345815) by @dsyme, shows that this is likely not how it should behave. I quote:

no implementation of let! ... and! ... should introduce calls to Async.StartChild, Async.Start or Async.Parallel - all of which start queue work in the thread pool. These calls must always be explicit. To be honest I feel it would be better if all of these had names like Async.StartChildInThreadPool, Async.StartInThreadPool and Async.ParallelInThreadPool. ANy introduction of the thread pool should be explicit

If it doesn’t apply to let! ... and!..., it certainly shouldn’t apply to let! in isolation. The method Async.StartAsTask forces a context switch and by above’s analogy is essentially Async.StartInThreadpoolAsTask.

To bring Don’s (and @gusty’s in that thread) point home: we should be explicit and opt-in to parallelism or context switches. Here it’s the opposite, we have to explicitly opt-out.

While this doesn’t introduce parallelism, it may have subtle behavior related to side effects or updating mutables and the like.

TLDR: we should switch to Async.StartImmediateAsTask (if, hopefully, this doesn’t introduce a backward compat issue we cannot come back from).

Repro steps

let currentBehavior() =
    let t = task {
        let a = Thread.CurrentThread.ManagedThreadId
        let! b = async {
            return Thread.CurrentThread.ManagedThreadId
        }
        let c = Thread.CurrentThread.ManagedThreadId
        return $"Before: {a}, in async: {b}, after async: {c}"
    }
    let d = Thread.CurrentThread.ManagedThreadId
    $"{t.Result}, after task: {d}"
    
let expectedBehavior() =
    let t = task {
        let a = Thread.CurrentThread.ManagedThreadId
        let! b =
            async {
                return Thread.CurrentThread.ManagedThreadId
            }
            |> Async.StartImmediateAsTask

        let c = Thread.CurrentThread.ManagedThreadId
        return $"Before: {a}, in async: {b}, after async: {c}"
    }
    let d = Thread.CurrentThread.ManagedThreadId
    $"{t.Result}, after task: {d}"

Expected behavior

No thread switch takes place. It should print "Before: 1, in async: 1, after: 1, after task: 1" in both cases.

Actual behavior

Two (!) extra thread switches take place. It actually prints this:

> currentBehavior();;
val it: string = "Before: 1, in async: 3, after async: 3, after task: 1"  // not good

> expectedBehavior();;
val it: string = "Before: 1, in async: 1, after async: 1, after task: 1"  // good

Known workarounds

Explicitly use Async.StartImmedateAsTask.

PS: this also applies to backgroundTask, perhaps even more so, as that already involves a context switch, so there’s even less reason to add another context switch on top of it.

Issue Analytics

  • State:closed
  • Created 9 months ago
  • Reactions:5
  • Comments:11 (9 by maintainers)

github_iconTop GitHub Comments

2reactions
T-Grocommented, Jan 2, 2023

Isn’t this what is wanted in the end? I must have overlooked this before. https://github.com/fsharp/fslang-suggestions/issues/1042

It is already implemented in the compiler’s internal test suite, could be moved to Fsharp.Core: https://github.com/dotnet/fsharp/pull/11788/files#diff-0b39085aa15d27c80662a1386282d6c4a9d2e8747b3124024d755bb96f9094f6

2reactions
T-Grocommented, Dec 22, 2022

Async.RunSynchronously has analogies to the backgroundTask{} in that behaviour, and I believe it was a good intention to prevent hangs when using it in UI applications.

Due to the amount of code using it, I do not think we can turn the behavior around. We could introduce a new variant of it that would not switch to a background thread, and name it differently.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Does async await increases Context switching
I am aware of how async await works. You are not. I know that when execution reaches to await, it release the thread....
Read more >
Task expressions - F# | Microsoft Learn
In a task expression, some expressions and operations are synchronous, and some are asynchronous. When you await the result of an asynchronous ......
Read more >
Task — Elixir v1.12.3
Tasks spawned with async can be awaited on by their caller process (and only their caller) as shown in the example above. They...
Read more >
I'll go against the grain here and say that async/await, ...
I'll go against the grain here and say that async/await, wether implemented by one thread-per-core like here or by stackless coroutines is not...
Read more >
Asynchronous : Async Await (.NET) Avoid Context ...
Lets look at a code snippet where context switch is not avoided! ... we discussed about Async Await which was used to improve...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found