21 ms·
Some thoughts on asynchronous Python API design in a post-async/await world
- justinsaccount 10y agoI feel like I'm too dumb to understand any of this. And I've been writing python for 12 years. Just give me greenlets or whatever and let me run synchronous code concurrently. async def proxy(dest_host, dest_port, main_task, source_sock, addr): await main_task.cancel() dest_sock = await curio.open_connection(dest_host, dest_port) async with dest_sock: await copy_all(source_sock, dest_sock) Are you kidding me? Simplified that is async def func(): await f() dest_sock = await f() async with dest_sock: await f() Every other token is async or await. No thank you.
- dismantlethesun 10y agoWelcome, the wonderful world of writing anything in Javascript. Imagine the same thing using Promises: def proxy(dest_host, dest_port, main_task, source_sock, addr): main_task.cancel()\ .then(lamdba _: curio.open_connection(dest_host, dest_port))\ .then(lambda dest_sock: copy_all(source_sock, dest_sock)
- noobiemcfoob 10y agoIt's statements like this that have kept me from ever learning _anything_ in Javascript...
- foota 10y agoI don't think most code will be this dense with await.
- sidlls 10y agoAnd Rust's developers think that 'unsafe' in third-party crates will be well-vetted and therefore actually "safe", most C developers don't think somebody will incorrectly free or screw with memory they've allocated and passed back to the caller, most C++ developers don't think anybody will (ab)use 'const_cast', and so on. A lot of terrible bugs in code is caused by people making assumptions such as yours.
- piotrjurkiewicz 10y agoHe didn't make an 'assumption' like those ones you described. This is an artificial example of a function copying unmodified data from source to destination. There are async and await tokens in every line, because every line is doing an IO operation. I a real world app this data would be somehow processed in between, using synchronous function calls, therefore without async/await tokens.
- imtringued 10y ago>most C++ developers don't think anybody will (ab)use 'const_cast', and so on. These constructs are opt-in. If you don't want them in your codebase you can find their location by a simple text based search and remove them. In C everything is "unsafe". You can't opt-out.
- steveklabnik 10y agoI don't think anyone is saying that you'll never see crates with bad usage of unsafe. What you will hear them say is that by having the ability to share code, since more people are looking at and using the same codebase, it's more likely issues will be found, and that when they're fixed, they help everyone using the package, rather than just those who found it.
- int_19h 10y agoI've been writing async/await code for the past 2.5 years, and no, it actually is typically this dense, if you count tokens (real identifiers are obviously longer, so it's not as bad character-wise, and awaits are not quite as prominent).
- foota 10y agoInteresting, thanks for sharing that insight. Do you feel that your work is representative, or is there some reason that the code you write would have a higher than usual density? It seems like a lot of code, which is just business logic, would not use these constructs other than on the io barriers.
- int_19h 10y agoDon't forget that async is "viral": if you call an async function and need to do something with its result, the calling function must in turn be async for await to work inside it. So the moment you start doing some async I/O at the bottom of some call stack, the entire stack becomes "infected" by async, and needs have awaits inserted in every frame. And it so happens that I work on the kind of products where a lot of useful work revolves around I/O: IDEs.
- Sacho 10y agoIt doesn't have to be viral. C#'s tasks have a blocking Wait[0] method which allows you to use an asynchronous Task without changing the signature of your synchronous function. The tradeoff is more verbosity. [0] - https://msdn.microsoft.com/en-us/library/dd235635(v=vs.110).aspx https://msdn.microsoft.com/en-us/library/dd235635(v=vs.110)....
- quotemstr 10y agoPeople keep inventing funky new ways of representing threads. With or without async, we're writing threads. (Promise chains are _also_ threads, very awkwardly spelled.) Really, we're arguing over whether we want our preemption points to be explicit or implicit. I prefer implicit myself, because the implicit style leads to much clearer code. I understand how the JavaScript people might be excited that they can finally have threads, even if ugly ones, but there's no reason to get the rest of the world to switch to explicit-preemption-point threads.
- int_19h 10y ago> Really, we're arguing over whether we want our preemption points to be explicit or implicit. It's not even that! It's not like you actually get to decide where to await in async/await code - you have to await on any call that is async, if you expect to get the result. Now, if the underlying framework uses hot tasks - meaning the async operation starts executing as soon as it's invoked, and not when the returned task is awaited (as in e.g. .NET/C#) - you can choose to omit async to, effectively, fork your async "thread". So NOT doing await on something is just a fork operation. It's the reverse from regular sync code, where thread forks are explicit, and sequential flow on a single thread is implicit. One other case where you wouldn't await is when you need to await on a combination of any or all tasks at the same time (i.e., wait until all tasks complete, or wait until one of the tasks completes). But the first one is equivalent to a thread join in sync code, and the second to a condition variable. So, again, you get a case where something more explicit in sync code is more implicit in async code, and vice versa. Now note that all this is solely about syntax! You can take the C# compiler, and change it so that every awaitable statement is automatically awaited, except when the newly introduced operator "taskof" is applied, in which case you get the raw future instead. Voila! Cooperative future-based multitasking with implicit preemption points. Yet it works exactly the same, and will even be able to call into and be called from any existing C# code compiled by the original compiler. I suspect that this will be the next step after async/await, once enough people notice that the default (non-await) behavior is something that they need very rarely, and figure out that it's better to rather change the syntax so that the much more common thing (await) is implicit. Similar to how the use of =/== for assignment and comparison has won out over :=/= in imperative languages.
- jeswin 10y agoAre you saying using greenlets are any simpler than this? IMO that mechanism looks way more complex compared to this. And will probably be less efficient. The point is this: threads are still expensive in bulk (the CPU has to shuffle a lot of data every time you switch). So all kernels have mechanisms to support parallel IO operations. An async library will use the best available kernel mechanism for IO; epoll on Linux, kqueue on BSDs, maybe IO Completion Ports on Windows (not sure). Turns out, doing that requires some help from the language itself or the code turns into a pyramidal mess. Async keyword addresses the readability aspect of code. So: a) It's more complex than synchronous code b) But it solves the performance problem without too much cognitive overhead (once you get used to it).
- quotemstr 10y ago> threads are still expensive in bulk They don't have to be. First of all, even ordinary threads are more efficient than you might think. On a really awful low-end Android 4.1 device, I can pthread_create and pthread_join over 5,000 threads per second. On a real computer, my X1 Carbon Gen4, I can create and join over 110,000 threads per second. (And keep in mind that each create-join pair also forces two full context switches.) For most applications, performance of regular threads is perfectly adequate. In these environments, the maintainability and debuggability advantages of using plain old boring threads makes it really hard to justify using something exotic. But suppose you do have big performance requirements: you can still use normal-looking threaded code. There's a difference between how we represent threads in source code and how we implement them. It's possible to provide green, userspace-switched threads without requiring "await" and "async" keywords everywhere. GNU Pth did it a long time ago, and there are lots of other fibers implementations. > the CPU has to shuffle a lot of data every time you switch Any green-threaded system (with or without explicit preemption points) also does context switches! Such a system maintains in user space a queue of things to work on: as the system switches from one of these work items to another, it's switching contexts! You have the same kind of register reloading and cache coldness problems that switching thread contexts has. There's no particular reason that you can do it much better than the kernel can do it, especially since switching threads in the same address space is pretty efficient.
- bufordsharkley 10y agoAfter watching (Curio creator) David Beazley's presentation from earlier this year on async/await[0], I feel I finally get it. Recommended watching. [0] https://www.youtube.com/watch?v=E-1Y4kSsAFc https://www.youtube.com/watch?v=E-1Y4kSsAFc
- insertnickname 10y agoThe amount of times Beazley says "insane", "nightmare", etc. in this talk makes me wary.
- BonoboBoner 10y agoWell it is only useful when you really rely on asynchronous programming. Nobody states that every piece of code is supposed to be written like this. You should only use async/await when a thorough performance analysis shows that it is your bottleneck. Think of handling a web request, where you have to do parallel I/O requests to subsystems like a database, a webservice, redis, and so on. I think async/await gives us a nice standard way of describing "hit me back once X is done".
- imtringued 10y agoIt's reasonable compared to the old way of having three layers of callbacks in Node.js.
- rudolf0 10y agoI still use gevent any time I need async code. It's also easy to tack onto existing projects with its monkey patching. I've never seen a need to migrate away from gevent, even if it's inarguably a language hack.
- tschellenbach 10y agoAre there any languages that have really nailed this? I've used gevent, eventlet, (both python), promises, callbacks (node) and none of them come close to being as productive as synchronous code. I'd like to try out Akka and Elixer in the future.
- jackweirdy 10y agoHow do you define "Productive"? Aside from that, personally I've used both Akka and plain Scala with Futures, as well as node with Promises, bare callbacks and async (though I've not tried fibers). I find Promises and Futures are the perfect balance between simplicity of use and the benefits of using the Async model. There's no need to reason about threads, as they abstract away the actual async implementation, and the interface they expose is very easy to reason about.
- mentat 10y agogoroutines with channels are well loved for concurrency in Go
- daurnimator 10y agoTry lua with cqueues http://25thandclement.com/~william/projects/cqueues.html http://25thandclement.com/~william/projects/cqueues.html
- quotemstr 10y agoC++? Java? Python? The traditional thread model isn't bad merely because it's traditional. I much prefer it to promise hell and to async-everything. About the only thing that beats it is CSP, which you can also represent sequentially without funky new keywords and which you can implement as a library for C++, Java, or Python. I never understood why people tout Go's goroutine feature so much. You can have it in literally any systems language.
- LukeShu 10y ago(I'm not terribly familiar with Python's threading, so I'm not going to talk about it) I never understood why people tout Go's goroutine feature so much. You can have it in literally any systems language. There are two big reasons for it. Firstly, goroutines are extremely lightweight. "Traditional" threading in C, C++, and Java means native OS threads, which are comparatively expensive. Sure, fiber/coroutine libraries exist for these languages, but they are far from common (and, the only fiber library for Java that I know of, Quasar, came after Go). Secondly, Go's ecosystem encourages CSP-style message-passing, rather than "traditional" memory-sharing. This is channels, not goroutines, but they make working with goroutines very nice. This is less concrete than the first reason; you certainly can implement message-passing in any of the other languages' threading styles. But empirically, it doesn't happen as often. A factor in this is also that, unfortunately, many CS curricula don't discuss CSP, which means that Go's use of this is the first exposure many programmers have to it.
- deleted 10y ago[deleted]
- quotemstr 10y agoThe idea espoused in this blog post, that > if you have N logical threads concurrently executing a routine with Y yield points, then there are NY possible execution orders that you have to hold in your head is actively harmful to software maintainability. Concurrency problems don't disappear when you make your yield points explicit. Look: in traditional multi-threaded programs, we protect shared data using locks. If you avoid explicit locks and instead rely on complete knowledge of all yield points (i.e., all possible execution orders) to ensure that data races do not happen, then you've just created a ticking time-bomb: as soon as you add a new yield point, you invalidate your safety assumptions. Traditional lock-based preemptive multi-threaded code isn't susceptible to this problem: it already embeds maximally pessimistic assumptions about execution order, so adding a new preemption point cannot hurt anything. Of course, you can use mutexes with explicit yield points too, but nobody does: the perception is that cooperative multitasking (or promises or whatever) frees you from having to worry about all that hard, nasty multi-threaded stuff you hated in your CS classes. But you haven't really escaped. Those dining philosophers are still there, and now they're angry. The article claims that yield-based programming is easier because the fewer the total number of yield points, the less mental state a programmer needs to maintain. I don't think this argument is correct: in lock-based programming, we need to keep _zero_ preemption points in mind, because we assume every instruction is a yield point. Instead of thinking about NY program interleavings, we think about how many locks we hold. I bet we have fewer locks than you have yields. To put it another way, the composition properties of locks are much saner than the composition properties of safety-through-controlling-yield. I believe that we got multithreaded programming basically right a long time ago, and that improvement now rests on approaches like reducing mutable shared state, automated thread-safety analysis, and software transactional memory. Encouraging developers to sprinkle "async" and "await" everywhere is a step backward in performance, readability, and robustness.
- vomjom 10y agoIt's not clear what you're suggesting as an alternative. My understanding is that you're suggesting thread-per-request, which has many known flaws. There are three approaches to serving requests: 1. Thread-per-request. This is a simple model. You have a fixed-size thread pool of size N, and once you hit that limit, you can't serve anymore requests. Thread-per-request has several sources of overhead, which is why people recommend against it: thread limits, per-thread stack memory usage, and context switching. 2. Coroutine style handling with cooperative scheduling at synchronization points (locks, I/O). This is how Go handles requests. 3. Asynchronous request handling. You still have a fixed-size thread pool handling requests, but you no longer limit the number of simultaneous requests with the size of that thread pool. There are several different styles of async request handling: callbacks, async/await, and futures. #2 and #3 are more common these days because they don't suffer from the many drawbacks of the thread-per-request model, although both suffer from some understandability issues.
- Animats 10y agoThe main use case for all this async stuff is handling a huge number of simultaneous stateful network connections. At least, that was what Twisted was used for. Are there other use cases for this sort of thing that justify all the complexity that comes with it?
- int_19h 10y agoIt lets you easily write responsive UI apps without worrying about things like threads - you treat your app as a single conceptual thread, and use async IO operations on it by awaiting them. Since in practice every operation callback is a new item posted onto the event loop, this doesn't block said loop at any point, and UI remains responsive. So the developer can think in simple terms like "if this button is clicked, [await] download this file, then update this label and [await] send this email", instead of background worker threads with condition variables etc. In particular, WinRT heavily promotes this approach for UWP apps.
- TimJYoung 10y agoMy problem with this justification is that these problems have been solved for a long time with simple message passing. In Win32, you just post a message to a window handle from the background thread to notify the UI of status updates, etc. Yes, you do need to worry about shared state/locks if that "message" includes more than a simple integer. But, these are also solved problems and rarely require more exotic lock-less queues, stacks, etc. for the majority of applications that use these types of architectures for UI background processing because the performance implications are inconsequential. Using a shared stack that uses a simple critical section will work fine for managing the messages, especially since Windows now has critical sections that can use spin locks to help minimize context switches.
- int_19h 10y agoIt's a solved problem in a sense that yes, you can do it that way. But it's also more conceptually complicated, and much easier to get it wrong, which is evident by the fact that so many desktop apps on Windows still lock up occasionally. Not so with UWP apps.
- codethief 10y agoI find it surprising that noone here comments on the actual topic of the blog post: Namely that the internal implementation of asyncio is opaque at best and this unfortunately propagates upwards to the public API. Personally, I have taken a look at its source code a few times as well (to understand what my code was doing because the docs were lacking details) and I remember that the callback hell paired with those additional user-space buffers the author mentions really made it a major PITA to reason about. Now, why should anyone worry about asyncio's internals? Heck, if everything was working, I wouldn't mind, either. However, as pointed out in the blog, there are quite a few edge cases where it isn't. Plus, the documentation traditionally doesn't do a particularly good job at explaining things. Or is it just the fact that the API is quite confusing sometimes that has caused me to take a look at the source code more often than I care to admit? (Compare https://news.ycombinator.com/item?id=12829759 https://news.ycombinator.com/item?id=12829759) Whatever it is, the fact is that asyncio's internals do matter unfortunately. …which is why I was happy to hear that not all hope is lost and that someone created an alternative. Now, I haven't taken a look at curio yet, so maybe I'm a bit quick to judge, but I already found it very refreshing that spending not even a minute to read the documentation already left me with a good idea of how it works and how I can use it. Kudos to the author(s), I will definitely give it a try!
- systems 10y agoi think because it is really actually new i think perl 6 is only being considered as production worthy since 2016 also as of now, most people who used it complain it is slow give it 2 more years before you worry, and for now continue with python or whatever you like to use no one is in a rush to make perl 6 popular ... it is not a commercial project ... so don't bet your career on perl 6 ... yet