6 ms·
I recently moved some code base from py/django to c#/aspdotnetcore. C# probably has some of the best async primitives of any language. I still managed to be bi
by throwaway13337 6y ago
I recently moved some code base from py/django to c#/aspdotnetcore.
C# probably has some of the best async primitives of any language. I still managed to be bitten by the very common issue of error logging getting lost in a hidden callback that doesn't look like a callback because of the magic of the async keyword (a common issue in node as well).
I'm convinced that the async style of web servers is largely a step in the wrong direction. A leaky abstraction that we don't need.
A better model already exists - thread per connection - for the majority of what we want as web developers. There are some good things about allowing us to manage our own waits but those things are rare.
It reminds me of the choice of when to manage your own memory - usually we don't need to, but sometimes we do. Most languages programmers use today decided, rightly, that memory management was an exception and not the norm. We are more productive today for the reduction in cognitive load for most applications as we decide not to manage our own memory.
Thread per connection has gotten a bad name for performance which is pretty false. You can create the exact same underlying abstraction with green threads for more concurrency when you absolutely need it (but mostly you don't). The code we get from not using async is reduced in complexity substantially.
I'm disappointed that django is on the async bandwagon. It means that I may have to make another framework choice in the future.
- GiorgioG 6y agoAs a C#/.NET developer I couldn't agree more - more often than not most of us simply do not need async, but everyone's doing it so it's become a 'best practice.'
- merb 6y agowell the problem is that if you use it once it's probably easier to use it everywhere. most people understand it if I show them a task explorer or ps and call Thread.Sleep and Task.Delay. I've never had an Issue with C# async and before I was a big scala user and did not have probems at all.
- melolife 6y agoThe new async Razor engine in .NET Core is ridiculous. They threw out view helper functions and support for functional/monadic control flow for what, so you can asynchronously render a string? Insane. I suspect the actual motivation is that async is virtually required to address performance problems associated with the new tag helpers - which are themselves a terrible, terrible idea. Slow, brittle, surprising, wholly reliant on syntax highlighting, and then redundant because you can implement the same functionality using the existing C# syntax.
- collyw 6y agoAs someone who has been in the industry for 20 years everything seems to be getting worse. Like Kubernetes and SPA apps. People rarely need these (OK, high availability via Kubernetes is nice, but few people need elastic scaling).
- earthboundkid 6y agoThat was my experience with a spikey server that I had behind an ELB. It was better to just eat it and run a medium server all the time behind a CDN than to try to scale up and down from small to large when a traffic spike came. The scaling isn’t worth the effort.
- collyw 6y agoWe apparently saved some server costs by moving to kubernetes, but it cost at least 6 months of dev time. And we have had problems with large uploads and downloads, because the smaller instances can't handle them as easily. I am not convinced it was a wise choice, but it was made before I started.
- tester756 6y ago>I still managed to be bitten by the very common issue of error logging getting lost in a hidden callback that doesn't look like a callback because of the magic of the async keyword (a common issue in node as well). What do you mean, any example? I have code full of asyncs and never had problem with logging
- sbelskie 6y agoThe biggest culprit of this kind of thing is usually people doing something with Task.Run thinking it’s a free way to queue a background task.
- nemothekid 6y agoAnything that spawns an unbounded amount of POSIX threads will be bad for performance, so you need an M:N scheduler to execute threads. I feel like this is pretty hard to retrofit onto an existing language (e.g. I don't think you can write a pre-empting scheduler in pure Python as a library either). AFAIK, only Go does this well (with great results) as a result of it being pretty much baked into the language. I don't think it's an async bandwagon, I think async/await is the best way to get concurrency without requiring the whole language to opt-in (as the Rust devs discovered).
- thomasfortes 6y agoElixir does that too (more a BEAM thing to be more precise), the BEAM can spawn millions of processes without killing your server performance. But I agree with your main point, concurrency is hard if the language isn't built from the ground to be concurrent.
- YorickPeterse 6y agoI think it's not necessarily POSIX threads related. In theory there is nothing wrong with spawning tens/hundreds of thousands OS threads, and Linux does a decent job at this. But in practise you'll run into issues such as: * Different OS' handle things differently. macOS limits the number of threads per process (somewhere around 1500 I believe). Other OS' may be slower, or impose other limits * Because of this, sometimes spawning threads is pretty fast (e.g. 10 µsec). Other times it's super slow (I've seen threads take over 500 ms to spawn) * Context switches are still pretty slow across OS'. There's finally some work being done towards lightweight thread support in Linux, but I suspect it will take a few years to become useful I _really_ hope that one day we _can_ "just" spawn 100 000 OS threads without issues, as it would make many concurrency problems easier to solve. Sadly, I think it will take another 10-15 years.
- nemothekid 6y agoIn theory there is nothing wrong with spawning tens/hundreds of thousands OS threads, and Linux does a decent job at this. But in practise you'll run into issues such as: The other big problem on Linux is stack sizes - "hundreds" or "thousands" of threads isn't a problem - the C10K problem which proved the dominance of nginx's evented model vs. Apache thread-per-conncetion model was broken 20 years ago. Where it matters is when we reach 100s of thousands or millions of connections on a single node. The first issue you run into is the thread stack size - Linux has a hard default of 8MB; at 100,000 you start to hit OOM issues first. Go (and BEAM) has a ton of work to make this use case possible within the runtime. What I'm unsure about if Linux even cares about solving this problem - the threading model is pretty general and I'm not sure how important high performance connection handling is. At the end of the day, if you are really I/O bound, an async style, co-operative, event queue will always be more performant than a generic threaded scheduler like the one Linux has; meaning if you care about performance (like Django probably does) they would probably go with an async style scheduler even if Linux could support a million threads.
- dtech 6y agoI get where you're coming from, but I disagree. Async code has its traps, but they are very managable once you've learned them. I haven't found them on the level of manual memory management which is a footgun that even experienced devs keep firing. The benefits are also there, doing slow things in parallel like HTTP calls or database queries is trivial with async code and trickier in sync code. Of course you prefer sync code when you don't need it, but async/await style code doesn't prevent sync code. If you only support sync doing anything async because very cumbersome and dangerous, and doing so requires things like managing threads or locks yourself which is an even greater recipe for bugs.
- calpaterson 6y agoYou can of course do parallel HTTP calls or database queries with Python's existing threads (IO releases the GIL in Python) or via multiprocessing. You don't have to use locks either - straight parallelism can be done very easily with the Executors in concurrent.futures. There are a lot of good concurrency primitives in the threading library in the stdlib including conditions, semaphores and barriers. Of course best to avoid parallelism with shared data if at all possible (and IMO it doesn't really come up much).
- jordic 6y agoThe thing is that you can serve a lot of requests on a single core.. (and at least us we pay per core)
- calpaterson 6y agoYou can use multiple threads and multiple processes on a single core - that is the purpose of timesharing operating systems after all.
- jordic 6y agoFor example: https://discuss.tryton.org/t/wsgi-server-choice/2766 https://discuss.tryton.org/t/wsgi-server-choice/2766 https://mail.zope.org/pipermail/zodb-dev/2010-December/013897.html https://mail.zope.org/pipermail/zodb-dev/2010-December/01389... https://docs.rhodecode.com/RhodeCode-Enterprise/admin/tuning-gunicorn.html https://docs.rhodecode.com/RhodeCode-Enterprise/admin/tuning... https://github.com/benoitc/gunicorn/issues/1045 https://github.com/benoitc/gunicorn/issues/1045
- zerkten 6y agoAsync has a place, but as you say there may be a better option for the majority of developers. The problem is that many developers are not identifying their performance issues and instead just throwing solutions out there. Perf discussions with plain Python often devolve into a GIL discussion before anyone confirms if the problem is CPU-bound. I see many issues from developers where I have to walk them back to the start and spend time on identifying the problem. There are many reasons for this, but it's not necessarily the most fun or exciting thing for developers to do. There are still many artificial impediments because devs don't universally have the control over, or access to prod environments that startups may have. If this troubleshooting is a hurdle then sprinkling in some async starts to seem much simpler.
- quadrifoliate 6y ago> I see many issues from developers where I have to walk them back to the start and spend time on identifying the problem. Honestly, in a lot of cases when I have thrown away something and used something else because I just need to show that I'm doing something to develop a reasonably performant application rather than spending all my time optimizing performance. Spending a whole week optimizing your application's performance is surprisingly rare in the modern hustle-driven culture of "lean" startups and such -- cargo-culted by a lot of companies and bosses who have nothing to do with startups. I recently realized that the entire gunicorn / uwsgi world for example is filled with anecdotes about performance instead of simple, comprehensible benchmarks; and I thought they were one of the better ones. I suspect this kind of lack of performance focus by most of the users is a big part of why.
- fiedzia 6y ago>I recently realized that the entire gunicorn / uwsgi world for example is filled with anecdotes about performance instead of simple, comprehensible benchmarks Its hard to create a simple benchmark for something that has 100+ configuration options and can be run in dozens of fundamentally different ways. C beats Python though by a large margin.
- simonw 6y agoDjango agrees with you. The async design for Django is predicated on the idea that the vast majority of cases are better handled by threads - but very occasionally it is useful to run an async view for something that's very IO bound (calling other HTTP APIs most likely). If you watch Andrew's talk from PyCon AU last week he explicitly calls out that sync views are still better for most cases, which is why he invested so much effort in making sure they were not negatively affected by the addition of async: https://youtu.be/ibAmA4QQDhs https://youtu.be/ibAmA4QQDhs
- theptip 6y agoThis is something that lots of folks seem to be getting confused about, with benchmark posts coming up on HN asking “is Django async faster than X?”. For the API gateway use case this feature is really useful, and extends the usefulness of Django by letting it serve as a gateway for longer as your app grows and you start extracting services from the monolith. I think part of it is the idea that in principle calling your DB is IO - but in practice your DB is fast enough that your sync view is not spending much of its time waiting on that IO.
- eyelidlessness 6y agoThis is a weird claim. Database calls where even moderate business logic is done at the database level can have very surprising performance characteristics and can be difficult to optimize. Taking these calls off the main thread isn’t a solution but it’s obviously a significant IO burden that doesn’t need to be shared across all of the happy paths
- theptip 6y agoOn my production api about 20% of each request is spent waiting on the DB. That’s with a lot of low hanging fruit remaining for optimization. YMMV. But you don’t get a speed boost by using async to talk to your DB in most common/textbook usecases. Sure, if your DB queries are complex they might take a long time in pathological cases (eg the classic accidental O(N) database query caused by not joining properly in your ORM), but the fix is usually to fix your DB queries, not add async to your API worker. Note that simonw is a Django core dev, so when they explain what the design goals were for this feature you can be confident they are correct. I’ll just quote it here for clarity: > The async design for Django is predicated on the idea that the vast majority of cases are better handled by threads - but very occasionally it is useful to run an async view This is an empirical question though, and will vary depending on workload; if you are doing something funky with long-running stored procedure calls, then you might get a win with async (make sure your DB driver is async too though, the default ones in Django aren’t last I checked). Again, my claim is just that most use cases are not like that.
- fiedzia 6y ago>A better model already exists - thread per connection For that you will need a language with good support for threads, which Python definitely isn't (at least not CPython), so Django doesn't have any other option here. Native threads are huge improvement over async, but still not as good as green threads, which really are the best option here for 99% of usecases, abstracting whole async thing away. > Thread per connection has gotten a bad name for performance which is pretty false. With 64 or more cores being the norm, a thread with 2MB stack per connection requires 128MB just to exist (though I believe OS may offer some magick if you are not actually using it). This is probably 3-4x time what's required for green threads.
- Scarbutt 6y agoWith 64 or more cores being the norm, a thread with 2MB stack per connection requires 128MB just to exist Sounds good to me! Can you even find a VM offering that has a 64 core CPU and less than 32GB of RAM?
- fiedzia 6y agoI tried to search for it and and... well, google is so ad-infested that's its useless, but doesn't seem so. Anything with large core count comes with 192GB or more.
- Something1234 6y ago>>A better model already exists - thread per connection > For that you will need a language with good support for threads, which Python definitely isn't (at least not CPython), so Django doesn't have any other option here The thread per connection is managed by your application server. Why does the language need to have good thread primitives then? In fact I would argue you spawning and managing your own threads make your service less robust.
- fiedzia 6y ago> Why does the language need to have good thread primitives then? Because: a) we want application server to be written in the language we are using, so that we can control every aspect of how it works, instead of having some silo that developer will avoid because its so different b) those threads will share cache, connections, some preloaded data you may need and so on, you want a language that allows this to be done reliablyl c) those threads may need to create more threads (make parallel requests to 3 services and resize an image at the same time)
- nurettin 6y agoasync views aren't there because django is moving in that direction, they are there to make it easy to use async functions that already exist. I write a lot of functions that block on IO and a lot of them benefit from async event loops. Now I have a simple way of integrating that functionality into django views.
- lazyant 6y ago> A better model already exists - thread per connection - for the majority of what we want as web developers. Haven't we already seen in Apache vs Nginx that thread per connection will OOM the web servers under heavy traffic? (haven't used Apache in many years so this may be obsolete)
- drzaiusx11 6y agoApache moved from using a thread per connection to fibers years ago [1]. Fibers are simpler than green threads (cooperative multitasking via yielding) which should be illustrative of how good an idea the "thread per connection" model is. Although it could be argued that fibers are a type of primitive thread--so as long you don't mean OS thread the idea has merit. [1] http://httpd.apache.org/dev/whiteboard/process-model.html http://httpd.apache.org/dev/whiteboard/process-model.html
- nesarkvechnep 6y agoOS thread per connection? Nah. BEAM process per connection - hell yeah!
- collyw 6y agoWhen you say "threads per connection" do you mean "process per connection"? There isn't a lot shared between the connection in the way that I think of threads. Anyway, I agree that async seems to be more trouble than it's worth for most cases.
- devxpy 6y agoThis. I really agree with this. The async afficianados also bring along the async ecosystem split. I wish they did a better job of bridging this gap. Languages like rust, which already have amazing support for threads don't need this split, please.