5 ms·
As a curious and perhaps naive aside, I've been wondering why the programmer should need to care about asynchronous execution of code at all. Can't it all be ab
by increment_i 10y ago
As a curious and perhaps naive aside, I've been wondering why the programmer should need to care about asynchronous execution of code at all. Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything? The advent of promises, async.js, and other paradigms tell me that people still kind of want to write code that does one thing after another, then another, then another.
- kraftman 10y agoOpenresty does this quite nicely with Lua: you just write your code as normal and it handles the rest in the background. http://leafo.net/posts/itchio-and-coroutines.html http://leafo.net/posts/itchio-and-coroutines.html
- klibertp 10y agoIo language does the same. The problem is that most languages are not powerful enough to easily express this. You basically need call/cc in some form or built-in coroutines. And a library/framework designed for this.
- romanovcode 10y ago>people still kind of want to write code that does one thing after another, then another, then another. For the most part - yes. And not because they want it but because there is no other way. You usually need to do something before you do that next thing anyway.
- insertnickname 10y agoThat's how it's done in Go. You write plain, "blocking" code, and if it blocks, the Go runtime will just schedule another goroutine (green thread). In Go there's no need for callbacks or littering your code with `await` keywords to write concurrent software.
- tluyben2 10y agoI never checked that out but ; https://www.golang-book.com/books/intro/10 https://www.golang-book.com/books/intro/10 looks messy to me. Less messy than callbacks but more messy than async/await imho. Maybe there are nicer examples? Also I agree with the coroutine LUA rationale you can read in the link in this same thread. It appears that with Go I still need to alter my own code to use libraries that are written to run async or am I wrong?
- insertnickname 10y ago>Less messy than callbacks but more messy than async/await imho. Maybe there are nicer examples? That book chapter doesn't really illustrate the utility of Go's concurrency very well, it just explains the basic components that make it work. Let's say you wanted to fetch three text documents via HTTP and print each of them. Here's the regular, blocking way to do that (error handling, package imports, etc. omitted for brevity): func main() { documentURLs := []string{ "http://example.org/foo.txt", "http://example.net/bar.txt", "http://example.com/quux.txt", } for _, url := range documentURLs { response, err := http.Get(url) body, err := ioutil.ReadAll(response.Body) fmt.Print(string(body)) } } For each URL in the documentURLs list, make a GET request to that URL, read the body of the HTTP response, and convert it to a string (from an array of bytes) and print it. First it'll fetch the first document and print, then the second, then the third. Of course, we'd prefer not to wait for the previous request to finish before we perform the next, so let's make it concurrent. func main() { documentURLs := []string{ "http://example.org/foo.txt", "http://example.net/bar.txt", "http://example.com/quux.txt", } // `documents` is a channel of values of type `string`. // A channel is a safe FIFO queue. documents := make(chan string) for _, url := range documentURLs { go func() { response, err := http.Get(url) body, err := ioutil.ReadAll(response.Body) documents <- string(body) }() } for doc := range documents { fmt.Print(doc) } } OK, let's see what's new here. First, we're making a "channel" which we will put our text documents into as we receive them. Second, the loop over documentURLs is a little different. We put the code into an anonymous function and run it with the `go` keyword. This starts the function in a "goroutine", which is like a light-weight thread. Because we run the function in a new goroutine, the loop does not wait for the anonymous function to complete and the loop continues immediately to the next URL, for which a function is again launched in a new goroutine, and so on for all the URLs. Anonymous functions are closures in Go, so we don't need to explicitly pass in the `documents` and `url` variables (actually this program is buggy, but that's a minor detail). In the closure we make an HTTP request and read the response body, just like before, but instead of printing the result directly, we send it to the `documents` channel. Basically, we start jobs on three new threads and tell them to put the result of the work in a queue. When we have started the jobs with the first loop, we proceeed to the next loop where we read the strings being sent to the `documents` channel and print them as we receive them. Reading on a channel blocks until there is something to receive. So I hope you'll agree that this is a pretty simple way to run things concurrently: just use multi-threading. Except we're not using OS threads, which are expensive, we are using goroutines—light-weight green threads managed by the Go runtime. You can have thousands or hundreds of thousands of goroutines running at once, OS threads don't scale that well. If you block a goroutine (e.g., when performing an HTTP request), the Go runtime will just schedule another goroutine, which is cheap. The Go runtime may use a single thread (in which case the program is concurrent but not parallel) or it may use multiple threads (in which the program is both concurrent and parallel). If instead you were writing a network server with Go, here's how you'd do it (example from https://golang.org/pkg/net/ https://golang.org/pkg/net/): ln, err := net.Listen("tcp", ":8080") for { conn, err := ln.Accept() go handleConnection(conn) } You listen for new TCP connections, and when you get one, you hand over the connection to a function started in a new goroutine, and then you go back to listening for new connections again. It's the simple model of one thread per connection, except with goroutines it's actually scalable. You can block as much as you want in the handler goroutine and it won't block other goroutines. You can even start additional goroutines inside your handler goroutines. For example, if you wanted to fetch the text documents from the concurrent GET example and send them to your clients, you could adapt the `main` function from the concurrent GET example just a little bit and use that as your `handleConnection` function. >It appears that with Go I still need to alter my own code to use libraries that are written to run async or am I wrong? You can make asynchronous library APIs with goroutines and channels: when a function is called, start the work in a goroutine and return a channel. The goroutine sends the return value on the channel. It's kind of ugly and generally frowned upon; it's cumbersome for users who want to use it synchronously, and it's no better than doing it yourself if you do want to use it asynchronously. Instead it is preferred to expose synchronous APIs, which can then be made to run concurrently as desired.
- _ZeD_ 10y agoThey are called threads. Have fun with that :)
- lmm 10y agoThe problem is how it interacts with other effects (e.g. mutable state): https://glyph.twistedmatrix.com/2014/02/unyielding.html https://glyph.twistedmatrix.com/2014/02/unyielding.html . I think one of the reasons people don't appreciate the importance of explicit effect management is that any one unmanaged effect in your program is usually ok. It's the interaction of multiple unmanaged effects that causes problems.
- buzzybee 10y agoFrom a mathematical perspective, synchronous code is strictly more powerful and say-what-i-mean. Asynchronous code is closer to how a (modern) computing system functions in practice. And yes, a large percentage of our use-cases for callbacks tend to be of the "yield execution of a multi-step task" form. But it isn't just callbacks, it's anything of a "concurrent-and-branching" type flow, so game AIs, UI, etc. also run into this stuff a lot. Sometimes it can be dealt with informally, other times it needs more structure for engineering with confidence to be possible. A formalized finite state machine is one solution to making sense of the problem in a more generalized way: instead of handing off execution flow based on a morass of polling and callbacks, there's a big switch statement to represent the FSM. The end of each branch of the FSM dictates what the next branch is by setting a branching variable. And every branch ultimately calls into the FSM again, either via a callback or an external loop. This style has the upside of an easier debug trace and the downside of another variable to track and potentially mishandle. (overall, a net gain as the FSM scales up) A more composable form of FSM used in game AI is the "behavior tree", which encodes blocks of logic in each node of the tree, each node returning a status code: success, failure, in-progress, and optionally taking an action affecting external state like "play animation" or "fire weapon". The general progression is a walk from roots to leaves, with some nodes used to determine the exact sequencing(left to right, pick random, etc.) and other nodes used to tell the tree to yield execution or restart processing by sending back a status code. With behavior trees, there's a substantial win for reusability since the nodes cleanly delineate their needs for parameters, allocation, yielding, etc. At the language level we're still mostly catching up on strategies that give the idioms comfortable syntax. Yielding iterators (such as those in Python) and promises can sometimes substitute for the simple case of calling a sequence. Full-blown continuations exist in several languages and are very powerful but also encode too much program scope to be used at scale or for long-running, persistent tasks.
- david-given 10y agoThat's exactly what monadic code in Haskell does --- a Haskell `do` block sets up a callback chain, and then when the runtime evaluates the chain, all the entries in the chain happen in the right order. Dependencies between items in the chain and between different chains happen automagically. Because all mutable state is encapsulated inside the monad and only actually takes effect when the state changes get applied to the outside world, it also allows really cool things like abandoning and retrying state changes if the state's not right. This allows really cool things like STM's 'atomically' operation. Behind the scenes it'll roll back and retry the operation whenever necessary --- but you never need to care: the effect of the operation gets applied to the outside world exactly once.
- dahart 10y ago> Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything? No. There's no way around understanding that some of your code will run now, and some will run later. It's imperative to understand this in places where you mix sync and async code. It's not possible to avoid mixing them, after all, the async functions have to be called by something. You could hide all async in a procedural layer if you accepted the constraint that once an async call starts, none of your own code will run until it returns. It wouldn't block the browser or OS, but it would block you. That's the only way to abstract async away, but that's a constraint I think most people would not be willing to accept. > ...people still kind of want to write code that does one thing after another I have no choice in the matter. I can't make use of the result of a REST call until I actually have the result. But, you're right at a fundamental level as well; people do want strict ordering and simple to understand execution with predictable results. The ideal is being able to read a piece of code and see & understand everything about it based on the function you're looking at, and not a bunch of context outside that function. It's the reason functional programming paradigms are favored by so many.
- kudokatz 10y ago> You could hide all async in a procedural layer if you accepted the constraint that once an async call starts, none of your own code will run until it returns. It wouldn't block the browser or OS, but it would block you ... I have no choice in the matter. I can't make use of the result of a REST call until I actually have the result. The core.async library for Clojure makes the code look imperative, but handles locking and selecting threads to run under-the-covers ... you may find it an interesting compromise https://www.infoq.com/presentations/clojure-core-async https://www.infoq.com/presentations/clojure-core-async
- hota_mazi 10y ago> > Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything? > No. Yes. That's exactly what coroutines achieve. They transparently suspend and resume execution of async code in order to make it look imperative. See https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md https://github.com/Kotlin/kotlin-coroutines/blob/master/kotl... for a good example.
- hota_mazi 10y ago> Can't it all be abstracted under a procedural layer and let the OS worry about not blocking anything? That's exactly what coroutines do. Here is an in-depth description of how Kotlin implements them: https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md https://github.com/Kotlin/kotlin-coroutines/blob/master/kotl...
- z3t4 10y agoFor most people thinking in the fourth (time) dimension should come naturally. It becomes complicated when there are multiple time-lines (threads) though, but JavaScript only has one time-line. You can have multiple time-lines in JavaScript via child processes and web workers though.