Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Forcing ConfigureAwait(false) on assembly level #2542

Open
ashmind opened this issue Dec 22, 2015 · 129 comments
Open

[Proposal] Forcing ConfigureAwait(false) on assembly level #2542

ashmind opened this issue Dec 22, 2015 · 129 comments

Comments

@ashmind
Copy link

ashmind commented Dec 22, 2015

Problem

It's recommended to always use ConfigureAwait(false) in certain kinds of libraries. While it's definitely possible to use an analyzer (e.g. ConfigureAwaitChecker.Analyzer) to catch those cases, the analyzer has to be installed separately and the resulting code is awkward and verbose.

Potential solutions

Option A

Provide an assembly-level attribute that would force compiler to generate (pattern-based) ConfigureAwait(false) calls for each await.

Option B
  1. Implement CallerMethodAttribute from automatic resource management for try/catch/finally statement for C# (duplicate for https://github.com/dotnet/roslyn/issues/4005, c# only) #351
  2. Add support for method info attributes to pattern-based GetAwaiter calls
  3. This would allow for a new overload GetAwaiter([CallerMethod] MethodBase method = null)
  4. Task could use this overload to look for some attribute on method.Assembly and return a correspondingly preconfigured awaiter.
@alrz
Copy link
Contributor

alrz commented Dec 22, 2015

@ashmind A quick question, does the following (from #114 comment) needs ConfigureAwait(false) on everyawait?

static async Task<TResult> UsingAsync<T, TResult>(this T disposable, Func<T, Task<TResult>> func)
    where T : IAsyncDisposable
{
    try { return await func(disposable); }
    finally { await disposable.DisposeAsync(); }
}

assuming that it is in a library.

@ashmind
Copy link
Author

ashmind commented Dec 22, 2015

@alrz I don't see why not, but I can't say I understand all the edge cases perfectly.
Let me know if I'm missing something here.

@paulomorgado
Copy link

Take this sample code:

void Main()
{
    SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
    Console.WriteLine(SynchronizationContext.Current?.GetType().Name);
    M().Wait();
    Console.WriteLine(SynchronizationContext.Current?.GetType().Name);
}

async Task M()
{
    Console.WriteLine(SynchronizationContext.Current?.GetType().Name);
    await Task.CompletedTask.ConfigureAwait(false);
    Console.WriteLine(SynchronizationContext.Current?.GetType().Name);
    await Task.Delay(10).ConfigureAwait(false);
    Console.WriteLine(SynchronizationContext.Current?.GetType().Name);
}

You'll find that the output is this:

WindowsFormsSynchronizationContext
WindowsFormsSynchronizationContext
WindowsFormsSynchronizationContext
null
WindowsFormsSynchronizationContext

ConfigureAwait(false) has no effect on completed tasks.

If a task is already completed, then the execution continues on the same thread (that might be a thread with a synchronization context). If you don't use ConfigureAwait(false) on subsequent calls, their continuation will be posted to the captured synchronization context.

So, to be effective, if you use ConfigureAwait(false) in one method, you'll have to use it on all awaits.

@HaloFour
Copy link
Contributor

I'd say that this is a CoreFX issue. ConfigureAwait is nothing more than an instance method on Task/Task<T>, the language provides no specific support for calling it.

@ashmind
Copy link
Author

ashmind commented Dec 23, 2015

@HaloFour But can it be implemented in CoreFX without option B support by compiler?
How would you know which assembly you are currently awaiting in?

@HaloFour
Copy link
Contributor

Considering how loose the awaiter spec allows for resolving the GetAwaiter method I'm kind of surprised that it doesn't allow for the method to contain optional parameters today. I'd be game for allowing it, and of course I'm all for the expansion of the caller info attributes.

@vermorel
Copy link

vermorel commented Jun 4, 2016

+1 It's indeed a pain to have ConfigureAwait(false) everywhere in practice for all libraries.

@NickCraver
Copy link
Member

I'd love to see a [assembly:TaskConfigureAwait(false)] added to the framework. People are working around this (but not in netstandard) with Fody today: https://www.nuget.org/packages/ConfigureAwait.Fody

async/await is great, but there are a few very big and very consistent pain points for library authors and app owners alike. At the top of my list are:

  • .ConfigureAwait(false) almost everywhere
  • Stack traces being incredibly noisy

We've had async for over 4 years now and neither of these has improved at all. Any language feature which requires repeating something verbose in almost every place is a bad experience. On top of that, forgetting to add the verbose thing (and many new to async do) defaulting to failure (overhead in this case) adds to the priority, IMO. Please, let's give library authors a break here.

...or we can make .ConfigureAwait(false) a VS 2017 built-in keyboard shortcut.

@petertiedemann
Copy link

Just want to confirm that this is a major pain for library authors. Installing analyzers into hundreds of projects and then having to set them all to Error severity just to ensure we add this ugly ConfigureAwait call is pretty de-motivating.

But another alternative could be to introduce another await-style keyword as syntactic sugar for await with ConfigureAwait ( false ). Not sure what a good name would be though.

@ufcpp
Copy link

ufcpp commented Feb 17, 2017

How do you guys think of an approach with Task-like (Generalized Async Return Types):
https://github.com/ufcpp/ContextFreeTask

This requires no IL-weaving or no new compiler feature.

@lanwin
Copy link

lanwin commented Feb 18, 2017

@ufcpp pretty nice solution.

Why they didnt added something like that to the TPL?

@asbjornu
Copy link
Member

I too find the current default maddening. What percentage does sync-requiring GUI code represent of all C# code being written today, anyway? Sigh.

@alrz
Copy link
Contributor

alrz commented May 31, 2017

..or we can make .ConfigureAwait(false) a VS 2017 built-in keyboard shortcut.

That's actually possible. We could add ConfigureAwait(false); to the completion list off the task.

So that would be C Enter.

@AgentFire
Copy link

That's actually possible. We could add ConfigureAwait(false); to the completion list off the task.

That'd still produce hude code redundancy.
Personally, I never use the same any line of code more than 2 times per solution. Code repeatedness makes my eyes bleed.

Looking forward to ThreadPool.MakeDefaultConfigureAwait(false); with a desperate hope in my eyes.

@HaloFour
Copy link
Contributor

@AgentFire

Proposals for BCL additions can be made on the CoreFX repository as any such helper method won't require language changes. You'd have to describe exactly what you expect it to do.

@AgentFire
Copy link

@HaloFour

here's no reliable way I see that this could be implemented purely in library; it would need to be based in language support. As such, I'm going to close this out. Thanks for the interest.

Meh

@HaloFour
Copy link
Contributor

@AgentFire

That's a proposal for an attribute, not a helper method. The failing of trying to manage that via an attribute is that it's far from obvious how the behavior is applied and when. A helper method that is invoked imperatively would not have that same issue.

@dominikjeske
Copy link

It would be great to have this - my code is polluted with lot of ConfigureAwait. Pleas do something with this – it should not be a big thing.

@enihcam
Copy link

enihcam commented Jul 11, 2018

Or maybe we can add a function-level attribute? For example:

[ConfigureAwait(false)]
public async Task FooBar() {...}

@dominikjeske
Copy link

It should be for method, class and assembly level ;)

@wachulski
Copy link

wachulski commented Jul 26, 2018

@ashmind What about:

Option C

Incorporate ConfigureAwaitChecker.Analyzer (counterpart) into native Roslyn analysers set with 2 diagnostics (and fixers):

  • Report when .ConfigureAwait(...) not explicitly set
    • opt-out
    • warning level by default
  • Report when .ConfigureAwait(false) not set
    • opt-in
    • error level by default

As for per solution/project configuration means new .editorconfig options might be introduced:

  • configureawait_must_be_explicit // false by default
  • configureawait_must_be_false // false by default
    Those would allow for opt-in/out up to project level basis.

@ashmind
Copy link
Author

ashmind commented Jul 26, 2018

@wachulski This is very practical, but I feel we should have more ambition and aim for cleaner code.
It's like if instead of await we got an analyzer that lets us write ContinueWith() better.

@sharwell
Copy link
Member

sharwell commented Jul 27, 2018

Over the past year, vs-threading has reversed my view on this issue. I am very much looking forward to no longer needing to specify ConfigureAwait outside of rare edge cases where ConfigureAwait(false) is required for correctness.

@petertiedemann
Copy link

@sharwell Can you enlighten us on how vs-threading resolves this (it seems to be focused on applications, and i am doing libraries that can be used in any context)?

@sharwell
Copy link
Member

sharwell commented Jul 27, 2018

@petertiedemann A good starting place is this recent document:
https://github.com/Microsoft/vs-threading/blob/master/doc/library_with_jtf.md

The basic idea is a library that avoids the use of ConfigureAwait will execute code on the synchronization context(s) that appear at the entry points to the library API. If those entry points occur on resource-limited contexts (e.g. the single main thread), the caller is expected to use a deadlock mitigation strategy. If the library further uses JoinableTaskContext internally (as opposed to just the implicit dependency from using ConfigureAwait(true)), the caller is expected to use vs-threading as the mitigation strategy.

@effyteva
Copy link

effyteva commented Oct 5, 2018

Almost 3 years since the proposal,
Perhaps it's time to take care of this?
Assembly/Class/Method attributes could save tons of hassle.
There's a Fody addin for that https://github.com/Fody/ConfigureAwait, which we haven't tried, as we prefer a native solution, as modifying the IL doesn't seem like a reliable solution for us.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Oct 5, 2018

we prefer a native solution, as modifying the IL doesn't seem like a reliable solution for us.

If there's a working solution... why not use it? :) Is there actually something unreliable here?

@tmat
Copy link
Member

tmat commented Oct 5, 2018

@CyrusNajmabadi I have yet to see an IL rewriter that correctly preserves debugging information. Also EnC won't work if you IL rewrite compiler outputs. I generally do not recommend using IL rewriters.

@CyrusNajmabadi
Copy link
Member

It's seems like a particularly onerous restriction to eliminate this entire class of tools. It basically means that all of that functionality must always be built into the compiler. That seems unfortunate.

@tmat
Copy link
Member

tmat commented Oct 5, 2018

Generators would address that.

@CyrusNajmabadi
Copy link
Member

For example, ConfigureAwait.Fody writes instead of us, but you just have a variation of coding by hand.

Both sound reasonable to me.

but ConfigureAwait.Fody is not oficial way

As i said above, this doesn't rise to the level that an official way is necessary. I don't view the problem as significant enough to warrant anything beyond the current story. If we had a really good solution here, i'd consider it. But we're in teh state of:

  1. not something that really feels like a big problem
  2. no good solutions readily apparent

Given both of those. My position is to keep the status quo and leave this to the tooling space.

@vitidev
Copy link

vitidev commented Apr 5, 2021

I don't view the problem as significant enough to warrant anything beyond the current story.

Just deadlock because of the forgotten ConfigureAwait (false) somewhere in the depths of tons of method calls )
Because someone decided that deadlock is better than "access from another thread" even though it is much more difficult to find the reason for the deadlock . In the case of "access from another thread", the reason is always easy to find - the last await. For deadlock - we have to go through all methods chain to find the place of the forgotten ConfigureAwait

@CyrusNajmabadi
Copy link
Member

Just deadlock because of the forgotten

I never forget CA. That's what that analyzer is for. I don't need to remember/forget anything. The tools take care of this for me. That's why i view this better as a tooling problem

Because someone decided that deadlock is bette

We decided that it's much saner and meets the majority case to come back to the callign context. My own experience here tells me that this is appropriate and desirable :)

@jnm2
Copy link
Contributor

jnm2 commented Apr 5, 2021

Deadlock is not the fault of a missing ConfigureAwait(false). Sometimes ConfigureAwait(true) is actually needed, and then what? It's the fault of code that decided to block the UI thread until something changes. The UI thread should never be blocked.

@vladd
Copy link

vladd commented Apr 5, 2021

Just adding my $0.02, the solution with await ThreadPool.SwitchTo(); (discussed here, akin to this one mentioned here), works like a charm. Having one extra line per method is a low price, and makes the programmer's intention very explicit.

I'd prefer the solution without an explicit way to restore the original context in order to not endorse "an unstructured/unscoped way of hopping between contexts" mentioned here: the switch to the thread pool should typically happen once at the beginning of a public method.

@vitidev
Copy link

vitidev commented Apr 6, 2021

@CyrusNajmabadi

I never forget CA. That's what that analyzer is for.

How does the analyzer distinguish a forgotten ConfigureAwait from "it should be by default"?
So you always write ConfigureAwait explicitly. That is, you litter your code with numerous ConfigureAwait

The problem is not "who write ConfigureAwait" . The problem is the need to write a bunch of boilerplate code at all, which worsens readability

@jnm2

Sometimes ConfigureAwait(true) is actually needed, and then what?

If this is the UI context, then there is nothing to talk about.

Non-UI context should not assume that he is "alone in the universe"
Why do we use ConfigureAwait(false) everywhere? Because we assume that we are being called in some kind of synchronization context and we want to avoid its influence. Having your own context doesn't change anything. We must always assume the presence of an external context, if we do not UI

@HaloFour
Copy link
Contributor

HaloFour commented Apr 6, 2021

@vitidev

The problem is not "who write ConfigureAwait" . The problem is the need to write a bunch of boilerplate code at all, which worsens readability

It sounds very much like the problem is "who write ConfigureAwait". Someone needs to write it, because there are two options as to what to do and the intent must be stated. When the decision is whether to make developers write it 90% of the time or 10% of the time the decision is obvious and clear.

Shrinking that down to a single character embedded in the await operator doesn't remove that boilerplate and certainly doesn't improve readability. It would make it a lot easier to miss. But if your aim is to make it fewer characters, then write your own extension method that calls ConfigureAwait(false). You could get it down to a total of 4 extra keystrokes that way.

Because we assume that we are being called in some kind of synchronization context and we want to avoid its influence.

It's your decision to take that burden onto yourself. If those libraries are a part of a solution for an application it's not very likely that the library code will be called from under multiple contexts. It's also not particularly likely that it actually matters whether or not that library code executes within that context. If that context needs to marshal information but not jump threads then it matters even less.

This discussion is going in circles and there clearly isn't an answer that will satisfy everyone. Short of proposing actual changes it doesn't seem there is really anywhere for this to go.

@vitidev
Copy link

vitidev commented Apr 11, 2021

certainly doesn't improve readability.

Does removing ConfigureAwait not improve readability? Seriously? That's why there is a default ConfigureAwait(true), and not "force everyone to specify ConfigureAwait explicitly" because it improves readability.

then write your own extension method

I can write anything. I can even use Fody.ConfigureAwait . But I can only do this in pet projects.

It's your decision to take that burden onto yourself. If those libraries are a part of a solution for an application it's not very likely that the library code will be called from under multiple contexts.

It's just common sense not to predict the future. There should always be a clear contract with no side effects. Long-lived projects are regularly refactorings and any code can be moved in the future to a separate library(module) and reused in a context unknown at the time of code creation.

Also ConfigureAwait (true) is a little but useless overhead

@HaloFour
Copy link
Contributor

Does removing ConfigureAwait not improve readability?

No.

That's why there is a default ConfigureAwait(true), and not "force everyone to specify ConfigureAwait explicitly" because it improves readability.

That would be because it is the majority use case. There's no need to require the developer to declare their intent to perform the default behavior. This has nothing to do with readability, it has to do with the pit of success.

I can write anything. I can even use Fody.ConfigureAwait . But I can only do this in pet projects.

That doesn't sound like a technical problem.

It's just common sense not to predict the future. There should always be a clear contract with no side effects. Long-lived projects are regularly refactorings and any code can be moved in the future to a separate library(module) and reused in a context unknown at the time of code creation.

The question would be how realistically that situation will occur for any given project, and it's likely not very high. Using a tool to add in CA(false) at the time this project is refactored would be entirely reasonable and take all of 5 seconds.

@Luiz-Monad
Copy link

It's just mind-blowing and very sad that this is still an issue in 2020.
Microsoft, I don't want to be a jerk, but I truly believe in being honest and direct and I have to say that in my opinion you have drop the ball on this one and it's just not good enough.
You want feedback, you want to be open source, you want a community, well then don't ignore a major plain like this one for years. I get that it's hard to solve and maybe it's falling between two chairs and so on, but it has been years now!
Please, solve this. If I still have to ConfigureAwait(false) things when .NET 5 comes out, you'll have to explain to my kids why my brains are on the ceiling. No, seriously though, I'm happy with my choice to be a .NET developer and the general direction of .NET Core and .NET 5, but you need to fix this and you need to fix it now...
And no, ConfigureAwait.Fody is not an option, it doesn't calculate a new checksum and thereby making it impossible to upload the snupkg to NuGet.
Hoping for a status update and an ETA.

The only language that solved this problem of contexts in a sensible way is Kotlin.
In Kotlin you are kind of forced to use contexts everywhere explicitly, but you set them on the function type. Which is not different from one of the proposed solutions in 2017.
Which is having a ContextFreeTask type that does the correct thing.

F# doesn't have this problem because it has its own Async.
I just stopped using C# because of it, its so much better. I just came for this discussion to fix an old C# powershell CmdLet, I guess I'll rewrite it on F# then.

This is how F# does it .
https://github.com/dotnet/fsharp/blob/19ade780d6019e05c24e09dda145447760e258ba/src/fsharp/FSharp.Core/async.fs#L1665
example of using
https://stackoverflow.com/questions/5433397/f-purpose-of-switchtothreadpool-just-before-async-return

95% of the time you don't want to bother with the Thread code runs, so ConfigureAwait default should be changed to false by the means of a massive breaking change to the TaskParallelLibrary, I mean, there should be two versions of TPL, one for UI, and the other for all other uses.

We really need a UITask that does ConfigureAwait(true) and the normal Task does ConfigureAwait(false), and it is a breaking change in so far as the user has to change its use of Task to UITask. As you do when you use Kotlin, for example (regardless of their generators being called suspend).

@Luiz-Monad
Copy link

@HaloFour

here's no reliable way I see that this could be implemented purely in library; it would need to be based in language support. As such, I'm going to close this out. Thanks for the interest.

Meh

There's a reliable way. Have two different Tasks, we already have ValueTask now.
People should learn to have more kinds of "monads".

@cafenroy
Copy link

I suggest to introduce a new Keyword in the language ==> await2. That keywork would set .ConfigureAwait(false);

And at the project level, an option ==> ForceAwait2

@jnm2
Copy link
Contributor

jnm2 commented Oct 14, 2021

bwait

@jeme
Copy link

jeme commented Oct 29, 2021

I would also certainly love to see something akin to [assembly:TaskConfigureAwait(false)] as a solution, that would be backwards compatible and ease the pain greatly.

Library authors could apply that and those of us who would actually prefer to be explicit about needed the continuation to run on the same context/tread where it is needed, could just apply that to all that we do.

I am very curious about some of the all the defense made for the default chosen behavior here, to me it's one of the biggest design flaws of the TPL, and I certainly don't buy the "most people would be best off with the current default", not then, not now...

@Kir_Antipov At the time (almost 10 years ago) the data I had access to had a range. Application developers represented 80%-90% of the .NET developer community with 10%-20% being made up of library developers. Based on that, and the fact that all applications you could build at the time would need ConfigureAwait(true) it was set as the default. But even without real data lets just go through the intellectual exercise:

Hopefully though, most of those 80%-90% was working on something bigger than just a small little "All code in the GUI" application, and if so, then suddenly they where actually developing internal libraries. Even though a library is not shared in the world, and even if it's never shared in your organisation, doesn't mean you shouldn't treat it well.

And in terms of old patterns, why would I not want my BusinessLogic or DataAccess code to execute on the first ready and available thread? Why would I at all want to lock my application to only ever execute on the GUI thread in any other places than where it was absolutely needed? - That just doesn't make any sense to me. We jumped hoops before to offload work to other threads than the GUI, utilizing the CPU's cores as best we could.

So it's not about what type of developer there are most of, it's about what type of code there is most of to me, in the context of a windows forms application, if your code does not modify any form controls, draw on any surfaces etc. It's NOT UI code, and therefore should not require to execute on your UI Context, instead it should align to the recommendation for libraries, hence applying .ConfigureAwait(false) to all such code makes perfect sense to me.

Take this small, simple, dumb - but illustrative example.

using System;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace AsyncFormTests
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new DumbForm());
        }
    }

    public class DumbForm : Form
    {
        private readonly IContainer components = null;
        private readonly Button theButton;
        private readonly TextBox randomTextBox;

        public DumbForm()
        {
            theButton = new Button();
            randomTextBox = new TextBox();
            SuspendLayout();

            theButton.Location = new Point(5, 5);
            theButton.Size = new Size(160, 50);
            theButton.Text = "ClickMe";
            theButton.UseVisualStyleBackColor = true;
            theButton.Click += OnClick;

            randomTextBox.Location = new Point(170, 5);
            randomTextBox.Multiline = true;
            randomTextBox.Size = new Size(260, 50);
            // 
            // Form1
            // 
            AutoScaleDimensions = new SizeF(6F, 13F);
            AutoScaleMode = AutoScaleMode.Font;
            ClientSize = new Size(440, 60);
            Controls.Add(randomTextBox);
            Controls.Add(theButton);
            ResumeLayout(false);
            PerformLayout();
        }

        private async void OnClick(object sender, EventArgs e)
        {
            theButton.Text = await MyBusinessLogic.DoHardWork();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }
    }

    public class MyBusinessLogic
    {
        private static int count;

        public static async Task<string> DoHardWork()
        {
            await Task.Run(() => { /* Normaly we would not have Task.Run here, instead it would be: Run a DB Query, read a file, request something over the network */ })
                .ConfigureAwait(false); //Remove this, now the UI locks.

            //Now lets do some intensive work on that!!!
            DateTime start = DateTime.Now;
            while ((DateTime.Now - start) < TimeSpan.FromSeconds(5)) { }
            return $"COUNT: {++count}";
        }
    }
}

In that example, why would I not want to add that ".ConfigureAwait(false)"?, what would the need be?

I would much rather have had to do:

        private async void OnClick(object sender, EventArgs e)
        {
            theButton.Text = await MyBusinessLogic.DoHardWork().ContiniueOnThisContext();
        }

Because then it's explicit to me. This is the piece of code that has a special requirement, not the DoHardWork method, The DoHardWork method doesn't care if it's executed on the GUI thread or not:

  • If it is executed on the GUI Thread, it locks the GUI. But! it will still do what it needed to do just fine.
  • If it's instead executed on the next available threadpool thread, it will still do what it needed to do just fine.

Therefore, DoHardWork should not need to add anything special like ConfigureAwait(false).
OnClick on the other hand...

  • If it is executed on the GUI Thread, it will do what it needs to do just fine.
  • If it is instead executed on the next available threadpool thread, well then it will throw an Exception.

Hence... it should be here I mark that I have a special requirement, namely that i require this piece of code to execute on the GUI Thread.

That also means that it's no longer "MAGIC", And in clearly signals that there is a special case/requirement here.

As long as your library is intended for different sync contexts, you must know the sync context for each use case. ConfigureAwait(false) may crash the consumer app, while ConfigureAwait(true) may deadlock it. Neither is safe default.

The standing recommendation from microsoft on this matter is that library authors should probably always use ConfigureAwait(false) - so I am very eager to get some examples to support the above statement, which to me seems to go against the recommendations.

Besides, any library you use provides you with a "contract" (the contract is often implicit though, so you have to discover it), if you don't uphold that contract and it means your application crashes, well then that is on you. So if a library applies ConfigureAwait(false) to all it's await and that somehow breaks your application, then that is not really the libraries problem, it's yours. But I am still VERY curious to see some examples of this in this particular situation. And even if there is some, I am sure they are easily fixed from the consumer's perspective.

Now... If we talk about flipping the "default" behavior for all Tasks tomorrow, then obviously it would be a massive breaking change that will break applications all over the world. There is no doubt about that, that is why I think that introducing a [assembly:TaskConfigureAwait(false)] attribute that ONLY flips the default for awaits in codepath's inside that assembly is a reasonable way to go.

Where as solutions like ThreadPool.MakeDefaultConfigureAwait(false); is obviously a big NO GO as that would affect code that might be based on the old default. E.g. if you consume a Form Controls library that uses the TPL on some places.

The important thing is that whatever is done to ease this, is something that only affects code "in your control".

Sorry if this became a bit "ranty" .

@GSPP
Copy link

GSPP commented Feb 14, 2022

@jeme You could take this argument even further: Should there be a SynchronizationContext at all? Should there be any ambient context at all (ExecutionContext, TaskScheduler.Current, TransactionScope.Current, any kind of thread affinity, ...).

These things are super dangerous. They make code non-local, non-composable and the bugs are nasty.

But they are so convenient... I'm really undecided on this. I sometimes think that these contexts optimize for novice programmers and demo-ware in which the goal is to make things "just work". The more scale the codebase has the more it requires preciseness and modularity.

Creating a ConfigureAwait(false) default sure is possible. The attribute could use a new C# language feature that allows wrapping an await expression in an arbitrary other expression. The C# language would remain neutral and just provide the infrastructure.

@Luiz-Monad
Copy link

I would also certainly love to see something akin to [assembly:TaskConfigureAwait(false)] as a solution, that would be backwards compatible and ease the pain greatly.

Library authors could apply that and those of us who would actually prefer to be explicit about needed the continuation to run on the same context/tread where it is needed, could just apply that to all that we do.

An interesting approach would be to use a different version of the TPL library that always do that. There could be two versions of the same library sharing the same contract and you could choose between UI/Library by simply adding the correct reference to your project.
That would mean the context in which the configuration applies is the Assembly and the entire compilation and its pretty obvious which is which.
An example would be breaking part of the interface of the Task into TPL.Pool.dll and TPL.UI.dll in which both use a TPL.dll assembly with the common shared code.

I am very curious about some of the all the defense made for the default chosen behavior here, to me it's one of the biggest design flaws of the TPL, and I certainly don't buy the "most people would be best off with the current default", not then, not now...

Certainly, that totally was a design flaw. But asking for breaking changes in working code is a obvious no go. So that's why I ask for another new incompatible and potentially different version of the TPL that does the correct thing.
I'm presuming that we would have to recompile all source code of anything that uses Task either way with such breaking change.

Because then it's explicit to me. This is the piece of code that has a special requirement, not the DoHardWork method, The DoHardWork method doesn't care if it's executed on the GUI thread or not:

Seconded, explicit is always better.

As long as your library is intended for different sync contexts, you must know the sync context for each use case. ConfigureAwait(false) may crash the consumer app, while ConfigureAwait(true) may deadlock it. Neither is safe default.

Perhaps one could create a Roslyn analyzer/rewriter. On the library side, It would to detect all uses of Task with CA(false) and remove them and update the assembly of the TPL to the new assembly (for ex TPL_Pool ) which has CA(false) as default, if there was any CA(true) it will leave it. That shouldn't alter any behavior nor break anything, except it requires recompilation of the updated code.
Then on the UI project it would do the opposite, all uses of any Task that was from the previous referenced libraries are going to be injected CA(true) and you would use another assembly (aka, TPL_UI) with an UITask instead, so it would be very explicit and a compile time error not to convert between Task and UITask using CA(true) (or something else better named). That wouldn't alter any behavior that wasn't already incorrect, nor break anything that wasn't already broken. Its good overall, and also requires recompilation.
And for those who don't have the source code to recompile, someone could come up with a Mono.Cecil rewriter that does the same at CIL level and rewrite the binaries, or just such it up on the boundary by using CA(true) everywhere to convert between TPL_Pool.Task and TPL.Task or TPL.Task and TPL_UI.Task.

The standing recommendation from microsoft on this matter is that library authors should probably always use ConfigureAwait(false) - so I am very eager to get some examples to support the above statement, which to me seems to go against the recommendations.

I bet if some library is fooling with this, it would be better having its own Task library and Task Pool than using the shared one, in this case my solution would not work that well .
Perhaps using the old EAP (Event-based Asynchronous Pattern) to this bullshit library that used TPL and somehow break if not running on tasks in the thread pool would be a good way to make them compatible.
I've only seem one software that was doing it, it was the Microsoft RDL report designer and Crystal Reports both with where doing some crazy shit with COM apartment models, and to my disadvantage I was using them inside AspNet, I was literally deadlocking the Thread Pool, the solution was running the code single threaded in a new thread and manually synchronizing code by using an AsyncQueue of sorts, why did I ever try to use the Async methods... lesson learned. I though Contexts could solve it, but nope.
Oh, there's COM apartment models bullshit. Now that I remembered this, DotNet mistake with CA(false) seems like a walk in the park.

Besides, any library you use provides you with a "contract" (the contract is often implicit though, so you have to discover it), if you don't uphold that contract and it means your application crashes, well then that is on you. So if a library applies ConfigureAwait(false) to all it's await and that somehow breaks your application, then that is not really the libraries problem, it's yours. But I am still VERY curious to see some examples of this in this particular situation. And even if there is some, I am sure they are easily fixed from the consumer's perspective.

I would find it amusing if that happened, it means the library contract is badly designed, it has literal undefined behavior and just happens to work. If you get to this point, all bets are lost, you can either live with it and don't change anything, or rewrite all uses of Task library. Changing a task library is a big task, but arguably doable, if your software has proper tests, shouldn't be too bad.

Now... If we talk about flipping the "default" behavior for all Tasks tomorrow, then obviously it would be a massive breaking change that will break applications all over the world. There is no doubt about that, that is why I think that introducing a [assembly:TaskConfigureAwait(false)] attribute that ONLY flips the default for awaits in codepath's inside that assembly is a reasonable way to go.

As I said, asking for breaking changes in working code is a obvious no go.

Where as solutions like ThreadPool.MakeDefaultConfigureAwait(false); is obviously a big NO GO as that would affect code that might be based on the old default. E.g. if you consume a Form Controls library that uses the TPL on some places.

This is TERRIBLE, this will do more harm than good, don't ever think about it. Better literally to not do anything than creating Global State that configures behavior of asynchronous code.

The important thing is that whatever is done to ease this, is something that only affects code "in your control".

My solution of replacing TPL would only affect precisely what you choose to affect.

@Luiz-Monad
Copy link

Luiz-Monad commented Mar 21, 2022

@jeme You could take this argument even further: Should there be a SynchronizationContext at all? Should there be any ambient context at all (ExecutionContext, TaskScheduler.Current, TransactionScope.Current, any kind of thread affinity, ...).

These things are super dangerous. They make code non-local, non-composable and the bugs are nasty.

But they are so convenient... I'm really undecided on this. I sometimes think that these contexts optimize for novice programmers and demo-ware in which the goal is to make things "just work". The more scale the codebase has the more it requires preciseness and modularity.

Creating a ConfigureAwait(false) default sure is possible. The attribute could use a new C# language feature that allows wrapping an await expression in an arbitrary other expression. The C# language would remain neutral and just provide the infrastructure.

The perils of implicit hidden mutable state. Async code should be Pure, but that's impossible to control with C#...
Which is one of the reasons I'm migrating to F#.

If only there was better research on Dependent Types, you could tag the contexts in the type and it would be a compile time error to use some functions in some contexts, you could explicit code in the type the invariants you require. For example, this code needs to never run continuations in the same Thread because its a danger.
Or this code needs to always run sequentially on the same Thread, even if at a later time, asynchronously.

You could do that with generics, but it becomes a pain very fast without Rank-N types or Dependent Types. You would start getting insane things like CustomThreadPoolWithFourThreads<Transacted<DatabaseExecutionContext<Task<Result>>>>, some which would need to be homomorphic and the same type as Transacted<CustomThreadPoolWithFourThreads<DatabaseExecutionContext<Task<Result>>>>. This would be a totally ridiculous way of writing code.
It would be better to have something like:

async dependent Result doSomething (param) 
    where dependency is Transacted CustomThreadPoolWithFourThreads DatabaseContext Task {  //see that Task is also a typed dependency not a generalized type
/*code*/ 
}

then on calling, inside another async.

async dependent void onRun() when dependency is Task {
    var pool = ThreadPool(4);
    var result = await doSomething (SomeParam()) with TransactionScope(), pool, dbContext;
}
// or even if you are already inside some contexts
async dependent void onRun() when dependency is Transacted DatabaseContext {
   var pool = ThreadPool(4);
   var result = await doSomething (SomeParam()) with pool;
} // Task dependency context just flows, not even required because of async/await.

Actually you could even get rid of the async/await keyword, you know you are inside Task context.

But this is for future research... (if only I had a position in the Research Division, I would love to play with those things).

@HaloFour
Copy link
Contributor

@Luiz-Monad

An interesting approach would be to use a different version of the TPL library that always do that.

If your projects never need synchronization then you'd be fine simply never setting a SynchronizationContext, then you don't need to care about CA(false). If you're working on ASP.NET Core then this is already the case.

You call CA(false) then you're likely to be running on a project that does (or may) require synchronization. Replacing the TPL library entirely so that there is no synchronization would completely break those projects and would never happen.

@epeshk
Copy link

epeshk commented Mar 8, 2023

IMHO, this may be implemented like nullable context, with ability to enable/disable implicit .ConfigureAwait(...) generation on assembly level via project property, or per code-region via directives:

*.csproj:

<ConfigureAwait>false</ConfigureAwait>

*.cs:

#configureawait true
public static async Task ContextDependentMethod() { ... }
#configureawait restore

UPD: Ok, introducing additional context is bad idea, class/method attribute for changing assembly-level default will be better.

@MisinformedDNA
Copy link

That's why i view this better as a tooling problem

Two years later, and I don't see any default tooling in VS. I have to add the VS Threading library.

And if tooling knows to just blindly add it everywhere, then the compiler should be able to do the same and reduce the noise.

@jnm2
Copy link
Contributor

jnm2 commented Aug 8, 2023

It isn't designed to be blindly added everywhere, though.

@ufcpp
Copy link

ufcpp commented Aug 9, 2023

dotnet_diagnostic.CA2007.severity = warning

@austinw-fineart
Copy link

With the death of WinForms and WPF it should be safe to deprecate ConfigureAwait

@theunrepentantgeek
Copy link

With the death of WinForms and WPF it should be safe to deprecate ConfigureAwait

Someone should tell the millions of developers using WinForms and WPF that this is happening. 😁

More seriously, those technologies are far from dead - there are a huge number of actively maintained projects and products that still deliver huge value.

@MisinformedDNA
Copy link

It isn't designed to be blindly added everywhere, though.

If it's a non-UI library, then we are adding it blindly everywhere. We haven't had any issues yet and if we did, it would be the exception and we would be happy to explicitly set it. Even @CyrusNajmabadi says he uses tooling to add it everywhere.

@spottedmahn
Copy link

spottedmahn commented Nov 10, 2023

this might interest some folks: ConfigureAwait in .NET 8 by Steve Cleary

reference: dotnet/runtime#87067

@epeshk
Copy link

epeshk commented Nov 11, 2023

@spottedmahn nobody wants to write more ConfigureAwaits. SupressThrowing is quite useful to reduce exception handling overhead, though.

It would be nice to have an option (in csproj, or via assembly attribute) to generate assembly-level default ConfigureAwaits by Roslyn.

@jeme
Copy link

jeme commented Feb 6, 2024

@spottedmahn as @epeshk wrote, we I think most people posting here don't want more of that stuff but rather less!

While SupressThrowing could be seen as a useful concept, since we already are calling a method I can't see why this couldn't be done as an extension method of sorts. (e.g. perhaps kind of like https://github.com/brminnick/AsyncAwaitBestPractices/tree/main/src/AsyncAwaitBestPractices)

And that probably also goes for the ForceYielding as well - So I have a hard time seeing that the work that went into that change will pay off.

In fact, it just brings some new awfulness to the API, e.g. the following is invalid and will give you a runtime exception: (it compiles just fine! - although you may raise a warning to error to avoid that)

int result = await Task.Run(() => {
    throw new NotImplementedException();
    return 42;
}).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

So I am not sure, It feels more like a step in the wrong direction to me. But I don't know, maybe there is some benefits to having the API like this over a more explicit one as the first I linked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests