dotnet / csharplang

The official repo for the design of the C# programming language
11.06k stars 1.01k forks source link

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

Open ashmind opened 8 years ago

ashmind commented 8 years ago

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 #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.
pablocar80 commented 4 years ago

Right now there is a default behavior, by not calling ConfigureAwait at all. But if we use that, the compiler gives warnings that we should be calling ConfigureAwait(false) to override that default behavior. There seems to be a disconnect there, with a default behavior that is also a non desirable one.

CyrusNajmabadi commented 4 years ago

But if we use that, the compiler gives warnings that we should be calling ConfigureAwait(false)

There is no such compiler warning.

image

CyrusNajmabadi commented 4 years ago

with a default behavior that is also a non desirable one.

It's desirable to me much of the time. When i'm doing libraries it isn't. But for non-lib code, it works very nicely.

theunrepentantgeek commented 4 years ago

the compiler gives warnings that we should be calling ConfigureAwait(false)

There are a lot of static analysis libraries, such as Roslynator that produce that specific warning. In Roslynator, it's RCS1090.

Chances are you're seeing that warning because you've opted-in to using a library that generates it.

If the warning isn't valid for your context, disable it.

pablocar80 commented 4 years ago

Ok not a compiler warning exactly, a warning from the code analyzer. The default works for me too and I dismiss the warning. If you don’t include that analyzer, visual studio gives you a huge warning banner that you should be including it. Not a big deal, I can turn off this specific warning in the analyzer, though still feels like there’s a disconnect when you have the analyzer telling you that the default behavior is wrong and you should override it every single time.

CyrusNajmabadi commented 4 years ago

Not a big deal, I can turn off this specific warning in the analyzer, though still feels like there’s a disconnect when you have the analyzer telling you that the default behavior is wrong and you should override it every single time.

You should file an issue with that analyzer. This is not the repo for that. We do not control the behaviors and decisions that individual analyzer packages decide on.

blankensteiner commented 4 years ago

@HaloFour @CyrusNajmabadi

Thanks for the replies! I appreciate you taking the time.

I've seen (relatively few) problems going unsolved for years and it seems to happen when a problem generates multiple issues within multiple repositories. That makes it very hard to see who has the ball and what the overall status is. I don't know if Microsoft has any cross-team meetings to look at "over X-year-old" issues, but I think some coordination could be useful here. To me, it sounds like a problem that Microsoft is able to solve, the right people just need to be put together and focus on this. If there really is no good solution, not even one that can be defined by a cross-team effort, then that's fine, but then close the issues with proposals you can't/won't implement. Then there is at least some "progress" and a "result". Even better, elevate the problem to a "Super Challenge" and make a community-wide contest out of it or something, just to let it be known that there is a hard problem that will go unsolved unless something extraordinary is done about it (just a suggestion from a frustrated developer).

CyrusNajmabadi commented 4 years ago

I've seen (relatively few) problems going unsolved for years

I mean... there are literally several thousand issues in this repo. Most of them are multi-year long :) So it's the norm that nearly all issues and concerns raised are just not going to be solved (possibly ever).

I don't know if Microsoft has any cross-team meetings to look at "over X-year-old" issues,

Yes. The teams meet and discuss what they think warrants attention. This has not risen to the level of outweighing the other stuff that is our current priority

If there really is no good solution, not even one that can be defined by a cross-team effort, then that's fine, but then close the issues with proposals you can't/won't implement. Then there is at least some "progress" and a "result".

I don't see a need to close this. Perhaps something will come of it in the future. I don't want to stifle people's ability to communicate and discuss this issue, or to discuss the pros and cons of this particular proposal :)

and make a community-wide contest out of it or something, just to let it be known that there is a hard problem that will go unsolved unless something extraordinary is done about it (just a suggestion from a frustrated developer).

I don't really see what about this issue warrants that. This is just another issue. To some it may be super impactful, to otehrs it may not show up on the radar at all. I don't think we want to just take the backlog of hundreds/thousands of bugs and just say "hey... we're running a contents to find solutions" just because some people really find the current state of affairs highly unpleasant. :-/

Like i said before, i think there's a perfectly reasonable solution. @HaloFour has already showed what it is. Given a reasonable solution, and no actual language-proposals that i think are really great, i know that this isn't an area i would champion to move forward.


TBH, @blankensteiner, it's not clear to me why you just taking the approach i mention here would not be suitable.

blankensteiner commented 4 years ago

@CyrusNajmabadi

I don't really see what about this issue warrants that. This is just another issue. To some it may be super impactful, to otehrs it may not show up on the radar at all

I doubt that. What you are seeing here is feedback from those who don't mind voicing their frustration. Do you really know the number of developers who want this fixed? The number of those swallowing the fact that they have to use ConfigureAwait.Fody or manually setting CAF? I haven't met a developer who doesn't want this fixed on an assembly/project level but arguing this doesn't help the issue.

TBH, @blankensteiner, it's not clear to me why you just taking the approach i mention here would not be suitable.

It's a fine quick fix until a real solution is found. One where it's a switch/compile-setting or something that doesn't mean I have to insert some code everywhere.

This is my two cents to this discussions. If you feel that this issue is being given the attention it deserves, then let's stop the conversation here and just see when/how/if this is solved. Have a nice weekend.

jnm2 commented 4 years ago

I haven't met a developer who doesn't want this fixed on an assembly/project level but arguing this doesn't help the issue.

🙋‍♂️ Hi! Now you have 😀

blankensteiner commented 4 years ago

🙋‍♂️ Hi! Now you have 😀

April fools day arrived early this year :-)

jnm2 commented 4 years ago

I'm serious. One more subtle invisible thing to mentally track is not thrilling. It is what it is, and it's not even that bad. It takes more typing to propagate cancellation tokens.

CyrusNajmabadi commented 4 years ago

I haven't met a developer who doesn't want this fixed on an assembly/project level but arguing this doesn't help the issue.

Shutting down part of the argument because you don't like that perspective is not helpful. Yes, the problem becomes easy if we no longer have to be concerned about the things you don't care about. However, that's not how we can actually do the design.

I, for one, am absolutely not ok with a ambient switch changing the runtime semantics of my code. We have that in an another place in the language (present in 1.0), and it's viewed as a painful mistake we don't want to repeat.

CyrusNajmabadi commented 4 years ago

It's a fine quick fix until a real solution is found.

What makes it not "a real solution".

It's simple, effective, and totally clear in the code.

HaloFour commented 4 years ago

It's kind of a shame that the AOP qualities of source generators ended up being rejected. If this API existed in the BCL (or could be written by a third-party), it seems like it would be trivial to have such a generator rewrite async members to call this API before invoking the actual method, e.g.:

public replace Task<T> FooAsync() {
    using (Task.DisableSynchronization()) {
        return original();
    }
}
qrli commented 4 years ago

Inspired by @HaloFour : ASP.NET/Core allows Action Filter attribute to add logic before and after a function implementation. Python attribute also enables that. And C# does have attributes affecting function behavior, e.g. force-inline, but not adding logic. Of course, an AOP rewriter can detect an attribute and rewrite the code from:

[DisableSynchronizationContext]
public async Task<T> FooAsync() {
    await BarAsync();
}

To

public async Task<T> FooAsync() {
    using var _ = Task.DisableSynchronizationContext();
    await BarAsync();
}
daniel-liuzzi commented 4 years ago

I stumbled upon this right after seeing the announcement for source generators earlier today.

Introducing C# Source Generators | .NET Blog

Wonder if they could be the key to make ConfigureAwait(false) the default.

HaloFour commented 4 years ago

@daniel-liuzzi

I don't think source generators can be used to accomplish that. The generator can only add new source, it can't modify existing source.

daniel-liuzzi commented 4 years ago

Ah, I didn't know about that. Bummer! Oh well… 😐

And you're totally right. I missed that bit from their FAQ: (emphasis mine)

How do Source Generators compare to other metaprogramming features like macros or compiler plugins?

Source Generators are a form of metaprogramming, so it’s natural to compare them to similar features in other languages like macros. The key difference is that Source Generators don’t allow you rewrite user code. We view this limitation as a significant benefit, since it keeps user code predictable with respect to what it actually does at runtime. We recognize that rewriting user code is a very powerful feature, but we’re unlikely to enable Source Generators to do that.

pablocar80 commented 4 years ago

If the generator could create methods that overwrite existing ones, then we could create something that wraps every Task function with ConfigureAwait(false).

HaloFour commented 4 years ago

@pablocar80

Generators can't overwrite existing code. If the generator did emit a duplicate method that would only result in a compiler error because you'd have two methods with the same signature.

alexyakunin commented 3 years ago

I like the original proposal with a few modifications for the consistency:

The option should impact only the code generated by C# async state machine builder. When configureAwaitDefault != null the builder should additionally invoke ConfigureAwait(configureAwaitDefault) on whatever is awaited if ConfigureAwait method is available (or an extension method with the same name). This means you are free to call ConfigureAwait manually as well, and in this case configureAwaitDefault won't make any difference.

Thoughts?

alexyakunin commented 3 years ago

As for generators, I'd rather opt out for some standard solution (i.e. something that's supported by C# compiler), otherwise we'll end up with a plethora of non-standard ones here, which will make the problem even worse.

Kir-Antipov commented 3 years ago

Top most evil villains in history:

  1. That guy who thought .ConfigureAwait(true) by default would be a great idea
CyrusNajmabadi commented 3 years ago

That guy who thought .ConfigureAwait(true) by default would be a great idea

I prefer CA(true) as the default almost universally for all my personal work. It's ideal. For narrower cases, CA(false) is more appropriate. But it's for a more complex domain where i need to think about those sorts of subtleties.

CA(false) being the default woudl be utterly broken for some many normal and simple cases.

Kir-Antipov commented 3 years ago

I prefer CA(true) as the default almost universally for all my personal work

@CyrusNajmabadi, can you add some examples, please (if it's not very time-consuming for you)? Once you've already convinced me on one issue, maybe here you'll make some good points too :)

As for now, I only see a bunch of frustrated developers (myself included) running around their projects putting out CA calls. I don't quite understand why CA(true), which assumes our desire to stay in the sync context, takes precedence over CA(false) that doesn't care about it, which is applicable to most libraries as well as web projects

HaloFour commented 3 years ago

@Kir-Antipov

I don't quite understand why CA(true), which assumes our desire to stay in the sync context, takes precedence over CA(false) that doesn't care about it, which is applicable to most libraries as well as web projects

Because it is estimated that applications outnumber libraries by at least 10 to 1 and that if CA(false) was the default that a significant percentage of more code would require CA(true) than needs CA(false) today. I would also make the argument that those application developers would also tend to be more junior developers who would be less familiar with the nuances of threading and synchronization and more likely to make mistakes if they had to be aware where they would need to apply CA(true).

Kir-Antipov commented 3 years ago

Because it is estimated that applications outnumber libraries by at least 10 to 1

@HaloFour, is it really? 0_o Wow. Even at the glory days of the .NET Framework, I would hardly believe in such a ratio, but at the present time, it seemed to me that the number of libraries should certainly exceed the number of applications that depend on the synchronization context... Could you share a link to the research papers, please? I can't find any reliable source, but it seems really interesting

I would also make the argument that those application developers would also tend to be more junior developers

Not even a question, true for most of the cases that I personally encountered

Well, if 10 to 1 ratio is real, we, library-writers, are the ones to suffer (if not suffer at all is not even on option :))

jnm2 commented 3 years ago

Keep in mind that the original ASP.NET app model also required app-level awaits to capture the synchronization context, and ASP.NET Core dropping that is a more recent development. Also unit tests sometimes require capturing the synchronization context, depending on the framework. Reusable helpers, libraries and (most, not all) console apps are the exception when they are framework-independent (agnostic to any app model).

theunrepentantgeek commented 3 years ago

I don't quite understand why CA(true), which assumes our desire to stay in the sync context, takes precedence over CA(false) that doesn't care about it, which is applicable to most libraries as well as web projects

The answer lies in the history of the async/await keywords in C# - they were released in C#5 in 2012. The developers working on this stuff were doing so in 2011 (and earlier), a decade ago.

One of the motivations for the asynchrony feature (well, one of the primary ways it was marketed and demonstrated, anyway) was that it made it dramatically simpler to avoid user interfaces that locked up when developing applications using WinForms and WPF.

Both of those user interface frameworks have a baked in assumption that you only interact with UI elements from the exact same thread that initially created them. This rule is inviolable - break it and your application will crash, hard.

The libraries built to support asynchrony therefore ensured that execution returned to the original thread when tasks completed - to do anything else would have made the feature difficult to use to the point of being useless.

theunrepentantgeek commented 3 years ago

the number of libraries should certainly exceed the number of applications that depend on the synchronization context...

Have you heard of Dark Mater Developers?

Scott Hanselman posits (quite reasonably) that the developers you see bloggings, tweeting, posting on social media, attending conferences (where available) and so on are the slim minority of developers. The rest of them are working developers, delivering business value as a part of their 9-5 job, getting their job done and then heading home to enjoy other pursuits.

I'd suggest that the number of libraries requiring asynchrony would be vanishingly small compared with the number of WinForms and WPF applications being written and maintained right now in 2021.

I worked full time as a .NET developer for a bank between early 2004 and late 2015 and I wrote literally dozens of them that are still in daily use.

qrli commented 3 years ago

Well, if 10 to 1 ratio is real, we, library-writers, are the ones to suffer

Besides what others have said:

CyrusNajmabadi commented 3 years ago

@CyrusNajmabadi, can you add some examples

Every application i could write would prefer this. if i asynchronously handle something, i want to return back to the UI thread to go continue interacting with it.

Kir-Antipov commented 3 years ago

Keep in mind that the original ASP.NET app model also required app-level awaits to capture the synchronization context, and ASP.NET Core dropping that is a more recent development. Also unit tests sometimes require capturing the synchronization context, depending on the framework. Reusable helpers, libraries and (most, not all) console apps are the exception when they are framework-independent (agnostic to any app model)

@jnm2, I do keep this in mind, but that doesn't change in any way that the ratio of 10 to 1 is surprising to me, as there's no simple logical outcome that makes "reusable helpers, libraries and (most, not all) console apps" an exception and makes context-dependent apps the rule, and not vice versa. No matter how you say this, Libraries-to-Apps is still Many-to-Many


One of the motivations for the asynchrony feature (well, one of the primary ways it was marketed and demonstrated, anyway) was that it made it dramatically simpler to avoid user interfaces that locked up when developing applications using WinForms and WPF

@theunrepentantgeek, well, yeah, I understand the part of the argument about WPF, WinForms, maybe even Silverlight (I stopped using it way before async/await, but it was still alive back in the days, as I remember it clearly), but this approach kinda makes assumption that C# has no future outside of Windows*

Like, ok, nowadays a lot of code is sync context-sensitive (at the end of the day it will be consumed by either ASP.NET, WinForms, WPF or Silverlight anyway. Console apps are an exception, as their number is definitely smaller compared to other applications, I agree), but what about the future? OS-free environment encourages the development of general purpose libraries, so even if apps outnumber libraries now*, it will change in the future*


Have you heard of Dark Mater Developers?

Interesting article to read and to think about! :)

So, you assume that "dark matter developers" only (or mostly) develop applications. What about corporate libraries? Reusable code? And so on


The only library authors who suffer from this is those who create cross-framework libraries

@qrli, you said that like they're a minority :) I've indexed all 325626 (I don't know for sure why this number doesn't match one on the main page) libraries on the https://nuget.org (I do hope they won't blacklist my IP for this small DDoS xD), excluded from them all that depend or may depend on some context-sensitive framework (like WPF, ASP.NET and so on), and received 282141 libraries in the output. So roughly 85% of the libraries are context independent

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

I’m having trouble parsing "is intended for different sync contexts". Do you mean some GUI library, that targets several frameworks like WPF/WinForms at the same time, or some general purpose library?

If it's first, then yeh, maybe. If it's second, then I can't imagine such a scenario.

Having class library projects in an app solution still counts as app here, because they are specific for the app's choice of sync context.

Not true at all. My latest Avalonia app consists of 14 projects. And only 1 is context dependent - the app itself. Even VM project was built in such a way that it doesn't rely on the sync context


Every application i could write would prefer this. if i asynchronously handle something, i want to return back to the UI thread to go continue interacting with it.

@CyrusNajmabadi, what about libraries, if you write any?*


*P.S. - These statements are written on behalf of someone who believes there're more libraries than applications. You, of course, have shaken my confidence about this a little bit, but I've not found any verified calculations, so this issue remains in the category of "religion", not facts. At the end of the day, this ratio plays a role only in the question of whether .ConfigureAwait(true) as default was chosen correctly. And no matter how we answer it today, the problem still remains for a nonzero number of people, and it needs to be somehow solved

HaloFour commented 3 years ago

@Kir-Antipov

For every JSON.NET library there are millions of apps that consume it. Even for corporate helper libraries, there's no point to them if they're not used by more than one application. I don't have the numbers which led to this decision, but the .NET and C# teams did at the time based on feedback and VS telemetry. I'd easily put money on there being more WinForm internal business apps written every year than all libraries. And I'd wager still that the majority of libraries written are still context sensitive and intended to be consumed by a specific flavor of application.

And no matter how we answer it today, the problem still remains for a nonzero number of people, and it needs to be somehow solved

It is solved, by virtue of being able to call CA(false). Yes, this is intentionally more painful for the minority of developers, and it sucks to be in that minority of developers.

I'd also note that, as mentioned above, this isn't really a language issue. The C# language doesn't know anything about context, it only understands the awaiter pattern. What that awaiter does with context is entirely opaque to the language.

vitidev commented 3 years ago

.NET and C# teams did at the time based on feedback and VS telemetry.

What feedback s and telemetry can be on functionality that did not exist at that time?!

And why it was decided to do exactly ConfigureAwait(false) but not with something await? / await! / ^await etc

bernd5 commented 3 years ago

Nowadays it is not required to use Task - it would be possible to create another awaitable object with other default behaviour. This could be implemented in the runtime or a regular user library - similar to ValueTask. And it could be implicit convertible to and from Task....

HaloFour commented 3 years ago

@vitidev

What feedback s and telemetry can be on functionality that did not exist at that time?!

Feedback on the ratio of project types.

And why it was decided to do exactly ConfigureAwait(false) but not with something await? / await! / ^await etc

Because there was no need to make the language aware of it. Adding policy concerns only complicates the language and makes it harder to evolve. ConfigureAwait is just a normal method that exists on Task and a handful of other types that provide built-in support for synchronization. Other awaitables could offer entirely different methods to configure synchronization or other policy.

jmarolf commented 3 years ago

@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:

Suppose that everyone whoever writes an application (Which we will define as something they are responsible for the deployment of) also writes a library and publishes it online. This would be an amazing universe where:

If we instead say that every developer is not going to write a json library since one already exists then they means that the number of applications over libraries will be >50%.

Kir-Antipov commented 3 years ago

@HaloFour,

For every JSON.NET library there are millions of apps that consume it

Not every library is consumed by millions of users. Moreover, not every library is consumed by more than one application. So, it's still Many-to-Many and which Many is greater is still not obvious. And I don't know why are we still arguing about it, because:

1) I won't believe anything without scientific fact being provided. "These guys have private telemetry which says so" is not a scientific fact. Among which participants was telemetry going? In what time period did this happen? How was the final value calculated? How much has changed since then?

2) My belief (based on the personal experience) does nothing with the problem of this topic

It is solved, by virtue of being able to call CA(false)

No-no-no, stop it right there, sir. It's just like saying: "The asynchronous programming problem was solved by the Thread class" (A warning for the guy who is about to write that async != multithreading: this was exactly the point, thanks)

This was a completely different problem, the solution of which led to another, smaller (?) problem, and that's why we're all here

I'd also note that, as mentioned above, this isn't really a language issue. The C# language doesn't know anything about context, it only understands the awaiter pattern. What that awaiter does with context is entirely opaque to the language.

I completely agree. The problem is presented by the framework, so it would be nice if framework itself could fix this, or some new general purpose language feature may be introduced that can also help in solving this problem (that's my point, I don't like tricky compiler-behavior switches too)


@jmarolf,

At the time (almost 10 years ago) the data I had access to had a range. Application developers represented 80%-90%

I got it numerous times: there's some closed telemetry data from like 10 years ago that says so, nice, there's no need to repeat it that many times, it makes nothing.

Well, ok. Let's imagine that the telemetry gave the most accurate results and they're still applicable. Is 10-20% of .NET developers a small number? If they are a minority, do they have to suffer? If they are a minority, do they have no right for a better life? Is minority negligible? I don't think this is the politics of the modern world.

I can understand logic behind CA(true) as default, but I can't understand why there's still no built-in solution to make library developers' life easier.

Shouldn't we be more nice to library developers? The bare framework is of course still good on its own, but where would we be without all these fancy libraries floating around? Why, for example, is Python so popular? The beauty of the syntax, the speed of work (ok-ok, I'm done, for real now) and, of course, a huge number of the most diverse libraries. What's happening at this time in the world of .NET library developers?

I know a lot of people who eventually gave up on .ConfigureAwait(false), they simply decided that if your application has a synchronization context, that's your problem, not theirs. Not the best solution, don't you think? Do I blame them? Can I blame them? Of course not. CA(false) is terrible: you constantly need to keep it in mind, add it to almost each method call, and periodically wipe the blood pouring from your eyes because of the boilerplate code that is smeared all over your screen.

I even know a good developer who rage quit C#-programming because of this. The poor fellow literally had a mental breakdown after yet another .ConfigureAwait(false).

So, it seems like a lot of us have some "CA(false)-durability" :)

But even without real data lets just go through the intellectual exercise

What's so intellectual about this "exercise"? Finding out the way it's broken? Kinda easy for real "intellectual exercise".

Let's take a closer look at your "amazing universe" and write down the key rules: 1) All libraries and apps are unique (Almost all code is novel) 2) Every app depends on 1 library, which describes its business logic (I believe, 'cause it was not stated directly, but depending on some kind of random library in this situation seems way more ridiculous) (Suppose that everyone whoever writes an application [...] also writes a library)

If we instead say that every developer is not going to write a json library since one already exists

And here you apply an expression from the real world (so your interlocutors can lose the thread of reasoning while they are busy processing the joy of recognition: "Oh, hey, I use JSON libraries in every project too, seems alright") to your invented world with made up rules. Since this partially breaks the first rule (apps are no longer unique as business logic was fully duplicated, but libraries are) while you're focusing on the second, I'm sure this was done on purpose.

But even that's not all! You used the word "every" very well. So now you're basically comparing the length of an infinite series of constants (apps which business logic's described by json library) to the length of a series of one element (json library itself). And here even an idiot would agree that the number of applications is slightly greater than the number of libraries, which, in fact, is what you wanted to prove.

And this whole thing is called sophistry. Don't get me wrong, big fan, and you did it well, but it's not the right place to do such things, I believe


To end this post, I want to describe my position, because of which I was so sure that the number of libraries outnumbered the number of applications.

To put it simply and briefly, this belief is based on personal experience: every project (personal or corporate) that I have worked on over the past 5 years has "spawned" from one to several dozen new libraries. If you'll take the average ratio of libraries per application from my experience, you'll get something like 4.

In longer terms, you forgot one thing about libraries: they're the logical unit just as functions or classes. If a function becomes too large, we split it into several ones. Small functions are easier to maintain, easier to debug, easier to test (we don't have the complexity index for no reason). The same applies to libraries. Their goal is far from just reusing some code.

Even for corporate helper libraries, there's no point to them if they're not used by more than one application

So the existence of a library that is used by only one project is more than justified.


I hope we're done. If there're still people who want to prove to me that the number of libraries exceeds the number of applications, please re-read the first statement of the first paragraph of this message and do not clog the thread (unless you have actual numbers on hand). We've already gone offtopic too much

HaloFour commented 3 years ago

@Kir-Antipov

Solutions that make the lives better for library developers would certainly be entertained, although it's a steep hill to climb to get the language designers on board that TPL-specific policy should be something baked into the language. API solutions have been better received, such as the ability to clear the synchronization context for a specific scope, which can be applied at the library entry points and eliminate the need to call CA(false) at every other await. Something like this.

As for libraries v. applications, those who make the decisions had the data to decide which audience should have the optimized experience. You have your own anecdotal evidence that contradicts those decisions, and that's fine, but even if the team wanted to revisit it they couldn't without breaking massive amounts of code. And I would bet money that the telemetry still bears out that the majority of applications are internal WinForm business apps, by a very large margin.

HaloFour commented 3 years ago

@Kir-Antipov

In longer terms, you forgot one thing about libraries: they're the logical unit just as functions or classes. If a function becomes too large, we split it into several ones. Small functions are easier to maintain, easier to debug, easier to test (we don't have the complexity index for no reason). The same applies to libraries. Their goal is far from just reusing some code.

IMO, If you're using libraries specifically for the purpose of organizing code that would have otherwise lived in the application itself then the guidelines of adhering to CA(false) need not apply. This is also true for libraries that are reused across very similar applications. It's not the project type that matters, it's where that assembly is used and the threading restrictions with which you need to be concerned. If all you write is ASP.NET Core apps and your libraries exist to service those apps then forget that CA(true/false) exists.

Some will likely disagree with me on these points out of sheer abundance of caution, and they're not wrong. But IMO you shouldn't need to change the code you write only by virtue of where that code lives.

Kir-Antipov commented 3 years ago

@HaloFour

Solutions that make the lives better for library developers would certainly be entertained

And that's the good news

Something like this

I've seen this proposal before, and it's certainly better than the current order of things, but it's still not the real solution: we're replacing thousands of boilerplate-affected lines of code with hundreds of boilerplate-only lines of code

Well, about API changes: some people have previously proposed a new class that behaves the same as task.ConfigureDefault(false). Maybe this is the solution? It doesn't break existing code, it doesn't require new language features and there's no boilerplate involved.

async ContextlessTask Foo()
{
    await Task.Delay(30).ConfigureAwait(true);
    LetsAccessSomeUI(); // Ok
    await Task.Delay(30);
    // LetsAccessSomeUI(); // Exception
}

async Task Bar()
{
    await Foo();
    LetsAccessSomeUI(); // Ok
    await Foo().ConfigureAwait(false);
    // LetsAccessSomeUI(); // Exception
}

Which downsides it may have?

but even if the team wanted to revisit it they couldn't without breaking massive amounts of code

Nobody asks to reevaluate that decision (I hope), of course not. Breaking changes that don't make the impossible possible aren't worth it ;)

Some will likely disagree with me on these points out of sheer abundance of caution, and they're not wrong. But IMO you shouldn't need to change the code you write only by virtue of where that code lives.

I am the type of person who approaches the task in a more abstract way, so I assume that the library will be reused even if it's very likely it won't (but it often pays off). But it's fair point indeed :)

CyrusNajmabadi commented 3 years ago

but I can't understand why there's still no built-in solution to make library developers' life easier.

Personally, as a library writer, I find the situation totally fine. I write code without ever writing CA (except the occasional CA(true)). Then, at the end, I use an analyzer/fixer to add an the CA(false)s to everything I didn't annotate.

It's a single step and then I'm done. So, as one of the language designers, I just don't feel any sort of massive need to do something here. This is compounded by having not seen any good proposals that I like here that would even address the issue.

CyrusNajmabadi commented 3 years ago

To put it simply and briefly, this belief is based on personal experience:

That's fine. But that's your personal experience. The idea of driving this off of data is to make a better default for the ecosystem as a whole, with the understanding that those that don't fall into the default may have a small amount of extra work.

You fall into the other group, so this happens. It's ok if that's the case. As I mentioned, working in the roslyn libs is a major part off my job. So I too have to deal with CA(false). But my development approach is simply too let automated tooling handle it for me, since it is fast and so simple to do :-)

I genuinely almost never even think about it. The only time I do are when I actually need to carefully use CA(true).

jnm2 commented 3 years ago

ConfigureAwait(false) is an optimization, not a correctness issue. The incorrectness leading to the deadlock problem is in blocking the UI thread in the first place. This is true whether it's a call to task.Result or some other form of blocking wait for something to change.

The separate problem of running CPU-heavy code on the UI thread is a problem that already exists with or without ConfigureAwait. The library might even have ConfigureAwait(false) and still end up bottlenecking the UI thread:

var x = await SomeIOStuffAsync().ConfigureAwait(false);
// On the UI thread still, if the previous await happens to complete synchronously
CpuHeavyStuff(x);

There various ways to assign responsibility for this, but ConfigureAwait(false) only makes this kind of problem more intermittent without solving it.

vitidev commented 3 years ago

If all you write is ASP.NET Core apps and your libraries exist to service those apps then forget that CA(true/false) exists.

I’ve heard ConfigureAwait(false) is no longer necessary in .NET Core. True?

So to say that ConfigureAwait (false) is only needed in libraries is a bit presumptuous

HaloFour commented 3 years ago

@vitidev

I mentioned ASP.NET Core specifically, which doesn't use a SynchronizationContext and where ConfigureAwait(false) is basically a no-op as a result. If your libraries only ever run in such an environment then you don't need CA(false). If you're using .NET Core with WinForms or other UI frameworks then you might want CA(false).

I also think that @jnm2 makes an excellent point that CA(false) is really more an optimization. There are scenarios where you want to try to avoid resuming the coroutine on the UI thread, but it's almost never a requirement. Deadlocks would only occur if the code blocks, and that's already possible in UI apps that block or loop expecting a signal to happen that must be marshaled to the UI thread. Otherwise it's largely about trying to keep processing off of the UI thread so that it doesn't impact the responsiveness of the UI, and if that isn't a concern for your code then you don't really need to worry about it so much.

vitidev commented 3 years ago

If your libraries only ever run in such an environment then you don't need CA(false)

It doesn’t mean, however, that there will never be a custom SynchronizationContext or TaskScheduler present. If some user code (or other library code your app is using) sets a custom context and calls your code, or invokes your code in a Task scheduled to a custom TaskScheduler, then even in ASP.NET Core your awaits may see a non-default context or scheduler that would lead you to want to use ConfigureAwait(false).

I am not a fortuneteller. Even asp.net core project can consist of several modules, the future of which is unknown I want to make sure there won't be a situation where in the future someone will use my class and get a deadlock because I assumed it would never happen. Therefore, I always explicitly use ConfigureAwait(false) everywhere to be sure that my code works in any environment as expected.

CyrusNajmabadi commented 3 years ago

Therefore, I always explicitly use ConfigureAwait(false) everywhere to be sure that my code works in any environment as expected.

I do the same for the libraries i write. That said, i don't explicitly write it myself. Or, at least, i don't try to write out every single one of these. I just use the quickfix to apply it to the entire doc at once:

image

image