rspeele / TaskBuilder.fs

F# computation expression builder for System.Threading.Tasks

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Question on ContextInsensitive tasks

dsyme opened this issue · comments

Hi all,

A question for people who use TaskBuilder, in relation to https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1097-task-builder.md

As everyone might know, the proposed support for tasks in F# is very much based around the functionality of TaskBuilder and we started with the test suite (though the implementation is entirely new).

Now TaskBuilder supports ContextInsensitive tasks where ConfigureAwait is false by default, e.g. https://github.com/rspeele/TaskBuilder.fs/blob/master/TaskBuilder.fs#L352. However the task { ... } support in the F# RFC doens't include ContextInsensitive tasks.

So the question is - how important is it for inbuilt F# task support to support a builder (e.g. open ContextInsensitive ... task { ...}) for context insensitive tasks? Or is it ok for people to do this manually?

Personally I think moving away from the UI thread should be done explicitly, e.g.

task {
    do! Task.SwitchToBackgroundThread() // or whatever

    ... } 

or via explicit ConfigureAwait calls.

I use ContextInsensitive and would appreciate it also being in the built-in version.

There are two related but slightly different use-cases for ConfigureAwait (false):

  1. move from UI Thread to ThreadPool to unblock the user interface: I agree that this should better be explicit
  2. your work is not thread sensitive, and you don't care whether it continues on the ThreadPool

In both cases you should add ConfigureAwait (false). Ideally to every await call, but at least to the first one per CE.


If you write a lib component, the behavior can also depend on the host. E.g., in a WPF application you have a synchronization context, in a console app you do not.

Omitting ConfigureAwait (false) can mean that your component runs fine in a console app, but deadlocks in a WPF app.


I'd go so far and say that outside of UI code, ConfigureAwait (false) should actually be the default.

Therefore, it should be as easy and as painless to do the correct thing as possible.

Omitting ConfigureAwait (false) can mean that your component runs fine in a console app, but deadlocks in a WPF app.

I get your point, though the dual is also true - adding it may mean utility code doesn't stay UI-affinitized when the user expects it to ...

Anyway the default is ConfigureAwait(true) for better or worse, as it is in both C# task code and F# async.

...ConfigureAwait (false) should actually be the default.

The problem is that people are coming to tasks either from C# task code or F# async. Some use TaskBuilder.fs but only a few.

  1. C# makes it explicit, so people basing their expectations on C# samples will add it anyway

  2. F# async uses explicit indicators like do! SwitchToThreadPool() , also StartImmediate v Start etc. (which should have been called StartInThreadPool, and Parallel should have been ParallelInThreadPool)

I'm loathe to add a third different way of indicating context sensitivity at the level of an entire "open" statement right across a module. Either we should do it like C# does, or do it like F# async does, or let people make their own choice outside of FSharp.Core.

@0x53A thanks for taking the time to reply. I'd love to have more data points. Can you link your code?

I don't personally have to use ConfigureAwait very often in my day-to-day coding. When I use TaskBuilder I just open the default module.

However, my thinking when I added the ContextInsensitive builder was that people who do use ConfigureAwait(false), want to use it universally. Within a single async method if one awaited task is ConfigureAwait(false) and another is not, that's almost definitely a mistake, not intentional. And this typically extends to the whole source file or even whole project: either the code you're writing in that file is library stuff that doesn't care about synchronization context, or it's application stuff that does.

I figured it was pretty harmless to have it as an option, especially with the context-sensitive one being the autoopened default to avoid surprises for devs coming from C# async methods (I definitely believe that all our defaults should match C# behavior for sanity's sake even if they aren't the "best" defaults). And to be frank, I have such a soft spot for neat tricks with inline functions, I thought it was pretty cool that the compiler would pick the ".ConfigureAwait(false)" inline binds for tasklikes that support that method, but still smoothly fall back to the regular bind for tasklikes that don't.

The existence of things like Roslyn analyzers and Fody plugins that add ConfigureAwait(false) to everything suggests that this is something that at least a handful of C# developers are frustrated by and want a catch-all solution to.

Whether that is worth the testing, complexity, and maintenance costs for an F# built-in, I don't know.

Can you link your code?

I'm currently using it only with Giraffe, and as far as I know, it doesn't actually matter there, because asp.net core doesn't have a synchronization context. I'm still using the ContextInsensitive builder because I wasn't 100% sure, and it makes it more explicit.

In my current job I'm not using F# with a GUI app.


...ConfigureAwait (false) should actually be the default.

This was more of a moral statement than a technical statement. If you are a library developer, and you write a component that does not require the synchronization context, you should always use ConfigureAwait (false). There are lengthy discussions about this on github and/or reddit, and the main argument against is that it clutters up the code. Which is why it should be as easy and "clean" as possible to do the correct thing.

And as rspeele mentioned, you either need it, or you don't; in most cases this is a binary choice and should be consistent for all awaits in the same method/file/project.

I get your point, though the dual is also true - adding it may mean utility code doesn't stay UI-affinitized when the user expects it to

I don't think this is an issue in most cases, the ConfigureAwait (false) only affects the remainder of the current method (in c#) / current CE (in f#), but NOT anything up the callchain.

For example (I hope this is correct, my C# async is a bit rusty):

async Task Parent()
{
    // UI Thread
    await Child()
    // switched to UI Thread again after Child()
}

async Task Child()
{
    // UI Thread
    await Task.Delay(100).ConfigureAwait(false);
    // ThreadPool
}

The need for this is there, for example there are a bunch of proposals on https://github.com/dotnet/csharplang asking for this exact thing: dotnet/csharplang#2542


As one additional anecdote, I was using Akka.Net from a WPF application and got deadlocks. Apparently it was previously only used in web/console apps and no one noticed this before me. This was fixed by sprinkling CA(f) everywhere inside Akka

The need for this is there, for example there are a bunch of proposals on https://github.com/dotnet/csharplang asking for this exact thing: dotnet/csharplang#2542

Yes indeed! Thanks for this link :-)

F# has a few obscure open-able modules that very fe people end up using. Does anyone really do

open Checked

or

open NullableOperators

or

open NonStructuralComparison

? I think these are barely known, especially the last

Am I write in thinking that a ContextInsensitive builder could be defined from the outside, in terms of a default ContextSensitive one in FSharp.Core?

One thing to check out here is the behavior with regards to AsyncLocals/ThreadLocals in the different modes, specifically that the new rewrite mechanism respects execution context capture and restore per @NinoFloris's comment here.

For task all of this will happen properly if all user code runs in IAsyncStateMachine.MoveNext as AsyncMethodBuilder.Start does the appropriate set & reset dance here https://github.com/dotnet/runtime/blob/9a905533d739564c14306ba3349fd7f7d3ebb834/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs#L21

I always use context insensitive builder in my apps and find it very convenient that I don't need to write ConfigureAwait(false) everywhere.

commented

We use task builder with ASP.Net (Giraffe) and Azure Functions. We use context insensitive.

F# has a few obscure open-able modules that very fe people end up using. Does anyone really do

open Checked

or

open NullableOperators

or

open NonStructuralComparison

I had a quick search on docs, found the info on nullable operators, but couldn't find anything about opening Checked ( arithmetic-operators mentions Microsoft.FSharp.Core.Operators.Checked, is that the same ?), nor any docs on a NonStructuralComparison module.

is that the same

yes

nor any docs on a NonStructuralComparison module.

Some docs are here, but yes it should be in the language reference.

https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-operators-nonstructuralcomparison.html

Recently worked on a small GUI made with Avalonia.FuncUI which needed to make a bunch of calls to an external service using HttpClient. I was using Ply rather than the original TaskBuilder, but they're pretty similar. Not having to worry about the UI locking up because of a missing ConfigureAwait(false) was pretty convenient.

Subtly different behavior depending on choice of open statement sounds like an accident waiting to happen. On the other hand I can't see a way to reconcile maximum conciseness with explicit local declaration of behavior and can sympathize with library authors who do not mix behaviors and hence do not see much confusion. Perhaps it would be nice to have some "tool-tip" to warn of non-default behavior in that case. Ultimately it may be a question of "freedom of expression" which is a somewhat non-technical question, albeit one that requires resolution. We are using TaskBuilder in internal library as well as UI code. My 0.01c.

Another option is to have both "task {...}" and "backgroundTask {...}"

Though I find it frustrating that using ConfigureAwait(false) for backgroundTask still starts the task in the current thread, up to its first await point (if I understand correctly). I would prefer it did an immediate SwitchToThreadPool which is semantically much cleaner? I've never liked ConfigureAwait at all for this reason

How much would the thread pool switch hurt scenarios where the awaited value is immediately available?

backgroundTask {
    // ... stuff
   let! result = ValueTask.FromResult "blah"
   // ... stuff
}

How much would the thread pool switch hurt scenarios where the awaited value is immediately available?

It would hurt in micro samples. However it just seems such a shame to run the first synchronous part of a supposed "background task" on the UI thread just for the sake of a bit of CPU perf - the precious resource here is the UI thread - the programmer almost certainly just wants everything off the UI thread ASAP.

Any comments on backgroundTask { ... }? Naming?

Also there's a possibility we'll add valueTask { ... } or vtask { ... } (though it sort of requires a .NET Standard 2.1 dependency, or at least is only maximally efficient with that). So presumably that would imply backgroundValueTask { .. } if we really wanted to complete the matrix

Note you can create ValueTask via

task { ... } |> ValueTask

Ply also has unitTask { .. } and unitVtask for Task and ValueTask (non-generic). Ugh C#

I think backgroundTask is more intuitive than a lot of the previous terminology. It isn't at all obvious what ContextInsensitive or ConfigureAwait(false) mean until you go digging through documentation and blog posts trying to figure out why your code is broken. I haven't used Ply's more advanced builders so I can't comment on those unfortunately.

My plan is to implement backgroundTask { .. } like this:

type BackgroundTaskBuilder() =

    inherit TaskBuilderBase()

    member inline this.Run(code : TaskCode<'T, 'T>) : Task<'T> = 
        TaskBuilderBase.Run(this.Delay(fun () -> this.Bind(Task.Delay(1).ConfigureAwait(false), (fun _ -> code))))


[<AutoOpen>]
module TaskBuilder = 

    let task = TaskBuilder()
    let backgroundTask = BackgroundTaskBuilder()

So basically an immediate bind to Task.Delay(1).ConfigureAwait(false). It seems to work but there's probably a better way not using Delay. Let me know if you know one. Unfortunately Task.Yield() returns a YieldAwaitable that does not support ConfigureAwait(false).

After reading this blog post I'm wondering if it would be better to make the context insensitive builder the default, i.e. name it task and come up with some other name for context sensitive builder? It would also solve the problem that backgroundTask is either inaccurate name if it just uses ConfigureAwait(false) and thus starts the task in current thread or take a small performance hit by switching to thread pool thread. I don't know how big this penalty is, but I'm surprised that it is considered acceptable since performance has been such a high priority in task support work as far as I have understood. Might be that I'm missing something here and wouldn't mind someone explaining why context sensitive builder is a better default.

I don't know how big this penalty is, but I'm surprised that it is considered acceptable since performance has been such a high priority in task support work as far as I have understood.

For me it's acceptable because backgroundTask fundamentally says "I want to get off this thread ASAP" - e.g. that the UI thread CPU resource is more important than any other minor costs. For example backgroundTask { return fibbonacci 1000 } should obviously not block the UI thread it's executed on, just to make backgroundTask { return 1 } a tiny bit faster.

Regarding the blog post - I'm not sure I even really agree with his proposition that library code should use be context insensitive. I think it should be neutral (i.e. context sensitive if a context is present), or at least should be analysed for whether it makes sense as a thing in a UI single-threaded world. I think library code that goes to the background should declare it

(note in in retrospect I think the naming Async.Start, Async.Parallel etc. should best have been Async.StartInThreadPool, Async.ParallelInThreadPool etc. , or else Async.StartInBackground, Async.ParallelInBackground . Something more explicit).

For me it's acceptable because backgroundTask fundamentally says "I want to get off this thread ASAP" - e.g. that the UI thread CPU resource is more important than any other minor costs. For example backgroundTask { return fibbonacci 1000 } should obviously not block the UI thread it's executed on, just to make backgroundTask { return 1 } a tiny bit faster.

I agree it is not a problem when backgroundTask is used in application code, but if it is used in library code to cater possibly rare uses cases that require it then it means that all users of the library are affected.

Regarding the blog post - I'm not sure I even really agree with his proposition that library code should use be context insensitive. I think it should be neutral (i.e. context sensitive if a context is present), or at least should be analysed for whether it makes sense as a thing in a UI single-threaded world. I think library code that goes to the background should declare it

Do you have some example where it would be beneficial that library would not use ConfigureAwait(false). Isn't it enough that UI code is context sensitive so that continuation of a task returned from library code is run in the UI thread?

One further point in favor of using context insensitive task builder in library code. Testing or using library that uses context sensitive awaits in F# Interactive in Visual Studio is cumbersome.

#r "nuget: TaskBuilder.fs"

open System.Threading.Tasks
open FSharp.Control.Tasks.V2.ContextSensitive

// This could be library code that uses context sensitive task builder
let foo () =
    task {
        do! Task.Delay(1000)
        return 42
    }

// Deadlocks
let attempt1 =  (foo ()).Result

// Deadlocks
let attempt2 =
    foo ()
    |> Async.AwaitTask
    |> Async.RunSynchronously

// This works
let attempt3 =
    async {
        return! foo () |> Async.AwaitTask
    }
    |> Async.RunSynchronously

Though this is a problem only with .NET Framework based F# Interactive.

Edit: Simplified attempt3 to use return! instead of let! followed by return.

I am very much against making the context-insensitive builder the default. I think it's of utmost importance for the simplest, most obvious, easiest-to-find version of task { ... } to behave as much like C# async methods as possible. This means one less surprise. Surprises, even harmless ones, make people feel betrayed and wonder what other surprises might be lurking for them.
Also, it gives C# devs a starting point for "getting" computation expressions. let! and do! and return! are very intimidating at first. If you can map them to the familiar ground of async/await, and keep the list of caveats and yeah-buts as small as possible, that helps.


However, I am also not real excited about the backgroundTask implementation Don proposed. Many of the people using the context insensitive builder now seem to like it for performance reasons, and they use it everywhere. This backgroundTask is not going to be good for that -- the initial Task.Delay will far outweigh any hypothetical perf benefit from being context insensitive, especially if you use backgroundTask everywhere. So this is clearly not intended for that "I'm writing a library, my code doesn't care what context it runs in, put me on whatever thread is available first" use case.

But what about the use case of getting the code off the UI thread ASAP? I may be wrong but I wouldn't trust it for this either. Unless you know that every Task you invoke within your backgroundTask also is ConfigureAwait(false) all the way down, you can't be sure it won't try to return to the UI thread down inside one of those Tasks. Usually not a big deal as long as there's no fib 1000 in there -- but if it's a library you're calling into, how sure are you that that is the case? And even if you are sure, is that something you want your UI code to be depending on?

And on the flip side, say you are sure the Task is going to complete relatively quickly (<50ms), and you'd like to use its .Result synchronously. backgroundTask can't help you here either: if something a few levels down the stack in the Task isn't ConfigureAwait(false), you get the classic deadlock. Maybe it slips your review, too, because maybe that particular not-configureawaited-task only shows up on one branch of an if that you didn't exercise.

I don't like my UI code to be trusting in details of how the Tasks it calls from other libraries are implemented, so if I want to be sure I leave the UI thread, I defensively use Task.Run.

Hmmm. First, for backgroundTask, the Delay is removed and Task.Run is now used instead, see here. I'm not sure if that makes a difference to your analysis.

Note that, currently, ConfigureAwait(false) is simply not emitted anywhere in tasks.fs. I guess that shows my dislike for the whole mechanism, which honestly feels like a weird kind of poison.

Anyway, prejudices aside, at the moment either

  1. use task { ... } with context capture if present
  2. escape any context via backgroundTask { ... }

That's it. You can explicitly call ConfigureAwait(false) if you like for your task { ... } code but there's no point doing it inside backgroundTask { .. } code AFAICS.

On this:

Unless you know that every Task you invoke within your backgroundTask also is ConfigureAwait(false) all the way down, you can't be sure it won't try to return to the UI thread down inside one of those Tasks.

ConfigureAwait is only relevant if the SynchronizationContext is non-null. The SynchronizationContext is null on thread pool threads, used by backgroundTask { ... }.

So unless the tasks created and executed by backgroundTask { ... } explicitly capture and return to the UI thread, then I don't yet see how there can be a return to the UI thread? Even if the code for backgroundTask { } creates a task { . } that will be done on the background thread pool thread, and still have no SynchronizationContext hence no capture.

backgroundTask can't help you here either: if something a few levels down the stack in the Task isn't ConfigureAwait(false), you get the classic deadlock

I think there is no deadlock, given that the SynchronizationContext is null for backgroundTask? Could you write out a sample using backgroundTask that you think will deadlock and we can check? thanks

FWIW I can see that neither task { .. } nor backgroundTask { ... } capture the computational modality for the kind of task writing that's in Stephen Toub's post. As you can guess I'm a fan of declarative computational modalities that satisfy laws, and in this case the declarative computational modality seems to be this horror:

If this task happens to be started on the UI thread, then escape the UI thread at first non-immediate-return Await. Whoever is binding to this task is in charge of sync'ing back to the UI context.

It's additionally annoying that basic building blocks like Task.Yield() don't support ConfigureAwait(false). That means the spec is really weird, it's like

If this task happens to be started on the UI thread, then escape the UI thread at first non-immediate-return Await which happens to support ConfigureAwait pattern. Whoever is binding to this task is in charge of sync'ing back to the UI context

It is frankly a horrible, ugly, bug-prone spec for task writing because it leaves so much undefined and up to the mercy of timing and immediate-return specs and a single missing ConfigureAwait somewhere down the UI-thread task chain, but I can see how they got there and it is what it is.

So yes, this modality is declaratively different to both task { ... } and backgroundTask { ... }. I guess that would indeed imply a affineTask { .. } or contextInsensitiveTask { ... } or goToBackgroundOnFirstAwaitTask { ... } or open FSharp.Control.ContextInsensitiveTasks or something.

I think the fact that you are using Task.Run(...) for backgroundTask rather than being a ContextInsensitive builder with an initial Delay changes my view of it completely. I think I misread it initially and thought that it was only doing ConfigureAwait(false) plus making the task delay or yield before running the first line of synchronous code.

That does make it suitable for the use case of, from top-level UI code like an event handler, kicking off some task and being sure that it won't interfere with the UI (and can even be .Wait()'ed if that's something you really need). Like I wrote in my post, those situations are exactly where I would use Task.Run so this being essentially just another way to do that, makes it fine with me. I don't think it's necessary but it's not harmful.

My complaints about deadlock risk were based on the idea that it was using the ConfigureAwait(false) and initial yield or delay to try to get off the thread, which for the reasons you go on to describe, is a fragile way to write async code. I totally agree that ConfigureAwait is a ugly API and I really don't like any cases where it's used to attempt to achieve a difference in outcome (like "will this deadlock or not") as opposed to performance because it's so easy to miss one and break it.

Yeah, backgroundTask makes sense to me.

What would you think if the spec of backgroundTask was this (instead of always using Task.Run)?

backgroundTask { code } == if SynchronizationContext.Current = null then task { code } else Task.Run (fun () -> task { code })

So backgroundTask = backgroundIfNecessaryTask and then can be used to write affine library code as well (though is slightly more eager to get off the UI thread than task with ConfigureAwait - which as I've said might actually be a good thing in any case)

The point here is that with this spec the following would only call Task.Run once:

let t1() = backgroundTask { ... }

let t2() = backgroundTask { return! t1() }

It's possible people might think Task.Run gets called every time but over over backgroundTask would reveal the behaviour

I think it's of utmost importance for the simplest, most obvious, easiest-to-find version of task { ... } to behave as much like C# async methods as possible. This means one less surprise. Surprises, even harmless ones, make people feel betrayed and wonder what other surprises might be lurking for them.

This is a good point that I agree with.

It is frankly a horrible, ugly, bug-prone spec for task writing because it leaves so much undefined and up to the mercy of timing and immediate-return specs and a single missing ConfigureAwait somewhere down the UI-thread task chain, but I can see how they got there and it is what it is.

I think the worst part of task spec is that you cannot be sure that after first ConfigureAwait(false) the synchronization context is dropped, which is also the biggest reason why I suggested to consider making context insensitive builder the default.

What would you think if the spec of backgroundTask was this (instead of always using Task.Run)?

I think this is an awesome solution that addresses many things at once:

  • Performant and thus suitable for libraries
  • Suitable for offloading work from UI thread since part before first await is also run in a background thread
  • Much easier to understand what it does compared for example to contextInsensitiveTask

@hvester Thank you for your feedback, it's invaluable.

I've updated the spec and implementation to reflect this variation on backgroundTask { .. }

At some point we should write a blog about this too. It's a good example where F#'s CE support allows subtly different computational modalities to co-exist (e.g. passing CancellationToken implicitly, or this), eliminating whole swathes of complexity.

Gave my two cents over here fsharp/fslang-design#571 (comment)

@NinoFloris's comments are:

I think it shouldn't do this, backgroundTask screams 'everything inside of this will not run on this thread' yet a single while true at the start would now halt my thread.

I agree there's a potential naming confusion, though since this name is not actually in use I think we can control it's understanding to some extent through docs etc. But if we can come up with a better name I'd like that.

Randomly thinking about naming:

  • escapeContextTask { ... }
  • nonContextualTask { .. }
  • freeTask { ... }
  • backgroundTaskIfNecessary { .. }
  • affineTask { ... }
  • escapingTask { .. }

Even if we'd decide to keep this new semantics just checking SyncCtx is not enough. Tasks also respect TaskScheduler.Current meaning we must include another check to see if it's anything but TaskScheduler.Default.

Yes, I agree we should check TaskScheduler.Current given that's what's specified here: https://devblogs.microsoft.com/dotnet/configureawait-faq/

As a whole I like the spec, better than anything to do with ConfigureAwait.

How about threadPoolTask? It doesn't in my opinion scream (at least as much) that it runs on a different thread as backgroundTask and it should be understandable.

While I really like the name threadPoolTask, I don't think it's strictly true for this spec. This is also one reason I'm not the biggest fan of backgroundTask.

The main method of a console app does not have a synchronization context, so the current spec would run it synchronously.

Example:

let main argv =
    printfn "1"
    let t = threadPoolTask {
        Thread.Sleep(10000) // pretend this is a CPU calculation
        printfn "3"
    }
    printfn "2"
    t.Wait()

As a user, I would expect it to print 1, 2, 3.
As I understand the current spec, the task would be executed fully synchronous, and it would print 1, 3, 2.


A separate threadPoolTask would be useful, there is Thread.IsThreadPoolThread.

Hmmm I quite like threadPoolTask (with an appropriate change of spec to look at Thread.IsThreadPoolThread).

This might be a bad suggestion but here goes anyway:

Given a builder is just a type plus an instance, could we provide a TaskBuilder with optional config in the constructor that people can then use to configure as needed, assign to their own instance named in a way they like, with a few standard instances instantiated by default.

/// Normal task, comes in the core
let task = TaskBuilder()

/// Custom for a user, they chose naming -  Switch to background as the first step
let backgroundTask = TaskBuilder(SwitchToBackgroundThreadImmediately = true)

Then if people need weird variants, they can define their own types, and the complexity can be moved into the one builder, rather than lots of variants. I appreciate making a single builder type that can manage all the variants might be hard ...

@davidglassborow, I'm a bit task and F# CE illiterate, but I do like both:

  • FSharp.Core comes with a restricted set of task builders, tailored for sound "declarative computational modalities that satisfy laws" uses, paired with a great documentation / guidance at the canonical places (FSharp.Core code comments and https://fsharp.github.io/fsharp-core-docs/)
  • exposing a task builder with the actual options that enable "make your own task builder" scenarios

I'm not sure if committing to the second point is a must have for the first iteration of this feature, as it would probably require more mulling over and there is already sense of scope creep (the positive one I'm sure) due to the RFC having been split in 3 already AFAIU; nonetheless the suggestion is far from "bad" to me.

If you need to control the execution context (the observations of elements done by the observer), Rx provides the ObserveOn operator that lets you pass the scheduler that the emissions will be scheduled on.

let taskOnDisp =
    someTask()
    .ToObservable()
    .ObserveOn(DispatcherScheduler.Current)
    .ToTask()

another operator is SubscribeOn to control the execution context.