Hacker News new | past | comments | ask | show | jobs | submit login
A Conversation with Guido about Callbacks (oubiwann.blogspot.com)
68 points by vgnet on March 24, 2012 | hide | past | favorite | 45 comments



Callback-heavy programming is what you get when your language doesn't support better ways of controlling context. It's analogous to manual memory management: it's work that's better to outsource to a compiler or runtime, except in specialized circumstances.

For Python, you can get the benefits of asynchronous IO without callback hell if you use the Stackless fork.

A similar idea for Node is node-fibers.

Both let you build exception-safe coroutines.


Or just use gevent or eventlet. It's not necessary to use a whole different branch of python.

On top of that, gevent outperforms both twisted and tornado because the eventloop is libev, meaning it's written in C instead of pure python.


The main problem I see with gevent/eventlet is it has many of the weaknesses of callback and preemptive threaded code with none of the strengths.

- It can't use multiple cores, even if Python got the ability in the future, gevent code simply wouldn't work as intended.

- While gevent is not preemptive, context switches are implicit, that means you cannot write code without locks unless you want to do some handwaving. You have to know the entire system to know that a multistep operation that calls functions is actually safe.

On the net, gevent may be better than Twisted or Tornado, but I think that is saying more about how much those suck than how good gevent is.


> The main problem I see with gevent/eventlet is it has many of the weaknesses of callback and preemptive threaded code with none of the strengths.

It is intended to create an environment similar to preemptive threads, yet due to it being a networking library it is unlikely gevent users will have to use the semaphore and locking primitives provided with gevent.

Regarding the weaknesses of callbacks, I disagree and would like to see evidence supporting your case.

> - It can't use multiple cores, even if Python got the ability in the future, gevent code simply wouldn't work as intended.

This is pure speculation. If Python's multicore handling was better (existed?), it's likely gevent would be implemented similar to other systems where you have threads allocated to each core and the concurrency primitives can then be executed on cores according to some algorithm.

> - While gevent is not preemptive, context switches are implicit, that means you cannot write code without locks unless you want to do some handwaving. You have to know the entire system to know that a multistep operation that calls functions is actually safe.

While this is true, I don't think it is likely to occur. Gevent's creator agrees: http://www.gevent.org/intro.html#cooperative-multitasking

> On the net, gevent may be better than Twisted or Tornado, but I think that is saying more about how much those suck than how good gevent is.

You're an Erlang programmer. You have to have that opinion.

(sausagefeet and I are friends IRL so I know his biases)


> Regarding the weaknesses of callbacks, I disagree and would like to see evidence supporting your case.

The weakness of callbacks is the lack of multicore support. It is not pure speculation that gevent simply won't work in a preemptive environment if it wants to utilize multiple cores. The sentence right about this makes my point:

> yet due to it being a networking library it is unlikely gevent users will have to use the semaphore and locking primitives provided with gevent.

Why is that? It's because gevent context switches on I/O events, not preemptively, so you have some control over when context switches happen. You can depend on 'my_dict[foo] += 1' to happen atomically. If you want to utilize multiple cores with gevent, as code using it is written now, you can't because all of the code out there depend on operations between I/O events happening atomically. This isn't a secret in the callback/gevent world. On the contrary, it's presented as a selling point. No longer worry about those locking primitives those real threading libraries give you because we are single threaded. (Ocaml/Lwt has the same problem).

> While this is true, I don't think it is likely to occur

I'll concede it's far less likely. I'm curious how large gevent code bases are in the wild, a large application that one cannot fit in their entire head could very well have a lot of the same race conditions that a preemptively threaded application has.

> You're an Erlang programmer. You have to have that opinion

Really? I didn't realize when I started using Erlang I signed a contract saying that. I also write Ocaml, so I guess any criticism I levy against Java is a product of that too, not rational thought.


> The weakness of callbacks is the lack of multicore support.

You suggest that gevent won't work in preemptive environment if it wants to support multiple cores, alas I'm still not convinced as to why.

The greenlets are cooperatively scheduled coroutines, as we know, but with proper multicore support I don't see why they couldn't adapted to be preemptively scheduled. If we're imagining that Python now has multicore support, we can also imagine greenlets being updated to work with it. Assuming gevent would receive no modifications to work with such an important update to Python is why I accused you of speculating.

> It's because gevent context switches on I/O events, not preemptively, so you have some control over when context switches happen.

That's correct. They are cooperatively scheduled coroutines. If someone wants to context switch, they can write code that does it or wait for I/O events. The lack of an explicit yield statement does not prevent coroutines from switching. In fact, greenlets are quite powerful here, more powerful than what's offered by Twisted and Tornado, because they don't have to yield directly to the caller. You could imitate CPS which is impossible with the explicit context switch model.

I am not disagreeing with you on this point, btw, just trying to be more explicit for other readers about the cooperatively scheduled nature.

Interested readers might want to check this out: http://en.wikipedia.org/wiki/Computer_multitasking#Cooperati...

> I'm curious how large gevent code bases are in the wild

Well, eventlet itself was written to support Second Life and sits behind multiple big video games.

Gevent is in production use for many large companies, most notably Spotify. They are moving to gevent from twisted for performance and cleaner code.

> I didn't realize when I started using Erlang I signed a contract saying that. I also write Ocaml, so I guess any criticism I levy against Java is a product of that too, not rational thought.

My statement is facetious, but the reality is that Erlang got concurrency right and Python is trying to glue concurrency ideas into the language as an afterthought. If Python didn't have it's lousy GIL we wouldn't have to pull such tricks to work concurrency into a single threaded system. Alas, it has the GIL.

Now, to conclude some of my thoughts, I believe gevent is an excellent way to build web systems. The coroutines are short-lived, exist to fetch some data with an easy nonblocking interface and then concat some strings together to produce the web page / API output.

If folks were building CPU bound systems it would straight-up be a mistake to use Python for that. Ocaml could be a better choice, alas, as you say, it has the same issue with a GIL though the computations would execute a LOT faster.


All gevent code written today assumes that between I/O events all actions will happen atomically with respect to the rest of the program. That means you can safely do any multi-step operation on a shared resource you want without issue. The second you want to run 2 gevent threads in parallel this guarantee goes out the window. Gevent could be modified for multicore suppor if Python got it, but that still breaks every piece of gevent code written today. This is the same problem Twisted and Tornado have.


Gevent wasn't designed for multicore because it doesn't exist today. It's fair to assume some work would have to be done to continue using it safely.

Edit: Solve the problems you actually have.


And that's exactly my point, the entire framework doesn't scale to multicore without a lot of work, just like callbacks. Which was my initial claim.


That's entirely dependent on what the work being done is. It's typical for gevent users to use the built-in queue systems to pass messages between coroutines, similar to Erlang's actor model. I do t believe a lot of work would be required if gevent users stuck to this style, as gevents docs suggest.


The guarantees of the framework no longer exist when you want to do things in parallel. Sure, not every line of code using gevent would break from this, but that's not the point. Even people using queues are often still playing with shared resources, which would be unsafe in multicore still.


You, my friend, have made excellent points and I yield to you now.

I hope this conversation is informative for readers.



Will be awesome to see more of what we get from pypy. Eventlet will probably gain a lot too, for those with a taste for the implicit eventloop.


Can you find a source for that, I'd love to read about actual performance statistics between anything and the likes ok twisted and such.


Twisted has a benchmark suite and release-to-release tracking of performance information, so that we can be sure optimizations have a positive impact. http://speed.twistedmatrix.com/timeline/



This link is flawed. It points towards eventlet but gevent is significantly better. Not sure why Ted felt otherwise, but I speculate that it's because at the time of writing gevent was considered incomplete compared to eventlet.

To be clear: gevent is significantly faster than eventlet and offers the same functionality.



Twisted's event loop is faster than gevent and tornado. http://cyclone.io/


This test is for a pointlessly weak concurrency level of 25. Under serious load the difference is much more gigantic showing that Twisted can't handle the same load as gevent.


> Callback-heavy programming is what you get when your language doesn't support better ways of controlling context.

UI programming is callback heavy, but I doubt that any language will support "better" ways for that.

So basically UI programmers are used to callbacks, others not that much, and now that event-loop servers are becoming popular, we see confusion from people not familiar with events.

Server-side people just need to figure out how to use callbacks properly and everything is gonna be fine.

All people need to understand is to nest callbacks properly, nested callbacks are the things that confuse most people.

> For Python, you can get the benefits of asynchronous IO without callback hell if you use the Stackless fork.

You can't just abstract away every programing feature that seems hard and expect everything to be fine, it just doesn't work that way, Java developers are still suffering because Java tries real hard to abstract memory management.


So basically UI programmers are used to callbacks, others not that much, and now that event-loop servers are becoming popular, we see confusion from people not familiar with events.

I don't think that's right. There is a huge difference. UI callbacks ("onClick" and the like) execute synchronously. But the callbacks in event servers execute at completely different times. Indeed, they're typically nested inside code that has long since terminated. This is what causes the learning curve, not grokking events or functions-as-values, which any half-decent programmer can easily do in the unlikely event that they didn't a long time ago. Indeed, the way we write asynchronous callbacks is cognitively dissonant with everything we've been trained to understand about scoping. The idea of a nested scope that doesn't mean "contained within" or "covered by", but rather "at some unknown time later", is inconsistent enough with how programming languages normally work that it can easily trip you up even after you get good at it.

Asynchronous programming is a very different thing than e.g. browser UIs in Javascript. I've programmed those for years. It still took me a long time to get my head around async servers.


I don't think the comparison with UI is so apt. Sure, they both use callbacks, but the granularity is quite different. In UI callbacks, you don't need to insert your own "scheduling points" as you need in explicity asynchronous programming. Mostly, you write functions that reacts to the events generated by the UI, but within those event handler, your own code is mostly synchronous.

I wonder why you mean by using callbacks properly ? The critical point mentioned by Guido is error/corner-case handling: the idiomatic callback-based code I have seen becomes difficult very quickly. I have not seen a case where e.g. deferred-based code ala twisted has been simpler than gevent.


UI programming doesn't need to be callback heavy.

Rob Pike designed one of the spiritual ancestors of Go, Squeak ( http://doc.cat-v.org/bell_labs/squeak/ ) using the CSP model of concurrency specially to build (G)UIs.


Qt uses signals/slots instead of callbacks and it works just fine. That's a "better way" as far as I'm concerned.


The library referenced in the OP, gevent, uses greenlet rather than stackless to provide this.


Agreed, roughly.

Nobody particularly likes reading callbacky code, but it has a lot of power. What we don't have enough of is "structured alternatives" for our "goto."


Seems like one has to be very out of touch with modern approaches to concurrency to like callbacks. Sounds flamey, but I'm just not sure how one can look at Go, or Erlang, or even Ocaml/Lwt with syntax extensions, and think "yes, callbacks are superior to this".


Rather flamey indeed, but let me take the bait. I've been using callback-style programming in many applications, ranging from IRC bots to application servers and whatnot. I've also been playing around with Go for several months and tried very hard to implement something that is inherently asynchronous. I gave up after about two months, I just couldn't get used to the concurrency model. I always end up with either data races, deadlocks and have otherwise no good overview of the program logic. This is exactly what the article describes, and seems to apply to me as well.

Incidentally, while figuring out how to implement something inherently asynchronous in Go I looked at some IRC client implementations, and noticed how all of those used callback-style programming one way or another. That doesn't inspire much confidence. :(

All in all, I am not sure yet whether this is purely due to my inexperience with concurrent programming or whether I am just inherently incompatible with it. I'd love to be able to play with alternatives to callbacks in the future, but for now it's just way over my head for the things I want to do.


It appears, much like FORTH, callback coding rots the brain. I am surprised you had such issues with deadlocks in Go, it takes some effort to write code that deadlocks in Go (I don't mean that a deadlock cannot be shown with a trivial amount of Go code, but that the style of programming tends to lend itself to not having deadlocks).

> and noticed how all of those used callback-style programming one way or another.

What do you mean precisely here, because I think you are conflating two concepts. There is a distinction between using a callback as a unit of concurrency (do this async task and call THIS function when you're done), and genuinely being an event handler (call THIS function when you get receive a particular IRC event). I would expect to see the latter in Go for IRC code, not the former.

But there is another aspect of this too, beyond code looking prettier. Go/Erlang/Haskell scale to multiple cores as the runtime evolves to support this. Callback codes doesn't, it can't. This doesn't seem to be because callback developers don't see the value in multiple cores, Node has built-in support for spawning a VM per core. This is quite restrictive. Callback developers have to either limit themselves to solving problems that don't need to communicate with each other or use some other means to communicate between VMs. That doesn't sound like progress to me. But shrug, plenty of people don't neat things in Node so maybe it doesn't matter.


It's hard to pinpoint exactly why I was having so many problems with deadlocks. Most of the time they could easily be solved by having channels with an unbounded buffer, thus guaranteeing asynchronous behaviour. But since the Go authors intentionally left that out, I must be doing something wrong at a more fundamental level.

You're probably right about the IRC clients using the latter style of callbacks. But then it turns pretty much into the following style:

  irc_connect();
  register_event_x(callback_x);
  register_event_y(callback_y);
  while(read())
    dispatch_event();
Which is essentially an event loop with callback-style dispatching. The only major difference here and with regular callback programming is that the callbacks are allowed to block. But in my mind that only complicates things as I can't tell anymore whether I can receive a particular event at some time because I have no idea whether there are any blocking handlers going on. Of course, you can dispatch everything into a separate goroutine, but then suddenly I can't easily argue anymore about the order of message arrival and what effect that has on shared state. Of course, this example may be a bit vague and abstract, but these resemble the kinds of issues I have constantly run in to.

On the scaling on multiple cores: Sure it's a nice advantage when you're already used to concurrent programming, but for most things I've written so far it doesn't really matter. In the few cases that I had a component that required heavy disk I/O or computational resources, it was only a small and easily-identifyable component that could be implemented in a separate OS thread. I'm not familiar with Node, but both glib2 and libev allow spawning a separate event loop in each thread and provide mechanisms to communicate between each other (idle functions in glib, ev_async in libev). Those are for C, however, and I have to admit that callback-style programming isn't too convenient there due to the lack of closures and automatic garbage collection.


I would expect the code to look more like an interface and handing your interface object off to the goroutine in charge of the IRC connection. Or the other way and have a goroutine in charge of shuffling the bytes back and forth and when it parses a package it has a channel to pump the information down and then you receive it and work on it. Either way, this seems much more straight forward than callbacks.

> But in my mind that only complicates things as I can't tell anymore whether I can receive a particular event at some time because I have no idea whether there are any blocking handlers going on

Don't really get what you mean here. The select operator seems like it would solve the problem your implying.

> then suddenly I can't easily argue anymore about the order of message arrival and what effect that has on shared state

You can barely, sometimes not even, reason about the order of message arrival and it's effect on state in callbacks, though. At every point in a callback you have the entire state of the program to deal with, at least in Erlang or Go you only have the context of your local process/goroutine to deal with. I have seen plenty of callback code explode because, what seemed like, an obvious sequence of events got reorderd or sent before the previous work was expected to be done, or any variation, and the code didn't handle it properly. You might be thinking "well, duh, that is easy to fix and a silly mistake to make", but it isn't. In a shared memory, especially where mutability is the default mode of action, an event-driven framework can be represented as a function that takes a 2 dimensional matrix: 1 dimension is every variable in my program, the other is every event that can happen in my program. At every point in the program any variation of this matrix must be a valid state. In a language like Go, or Erlang, I can cut this matrix down significantly so that I only have to worry about the matrix involving some subset of events and some subset of variables. In short, that's a massive win IMO.


Not sure whether what you saw was an artifact of your long exposure to callbacks. The Go/Erlang kinds of approaches need you to think at a slightly higher level than callbacks. A small step is the concept of a "promise" .. which, in Go, can be modeled (roughly) as a channel on which some process waits to receive a value promised to it that is being computed in another process. At a higher level are streams of data. Do you have an example you found hard to express in, say, Go without thinking "callback"?


Surely Erlang uses callbacks heavily, in the sense that you pass functions around. Supervisors, for example, depend heavily on callbacks to receive notifications when something happens to the processes it supervises.


You're confusing things. Callbacks are not the unit of concurrency in Erlang, a process is, which is a sequence of steps. You implement callbacks for behaviors in Erlang, but that is an interface system, not a concurrency unit.


Well, I was referring to the "interface system", which is a pretty significant detail when you are comparing two programming approaches.

The fact that Erlang has a richer set of concurrency semantics — that, among other things, let you quite easily manage a graph of dependencies so that your callback logic becomes much simpler — doesn't change the fact that it relies on callbacks (something "calls back" a function in order to supply information asynchronously) as a fundamental unit of programming.

I don't know gevent in detail, but I suspect that it's possible to create an Erlang-style process/supervisor-style framework on top of it, albeit with less elegance and more syntax than Erlang. That would change the underlying concurrency mechanism, but the interface — callbacks — would remain very similar.


A callback in Twisted/Tornado/Node is a handler for the result of some asynchronous action happening. "Download this webpage and call ME with the results". A callback in Erlang is some functionality in a service a process provides. You call a callback to perform some task then it gives it back to you, possibly synchronously in the case of gen_server:call. Using a function in the Twisted/Node style would not make sense since a process is the fundamental unit of concurrency in Erlang so message passing is the only way to communicate back and forth. It would simply make no sense to ask a service in Erlang to "call THIS function when you're done" in most cases. The function would run in the wrong process and this isn't how you construct Erlang code. The semantics are completely different. A callback in Erlang is much closer to a webservices, for example, where you ask for some work to be done and it gives you the result. As far as I know, Gevent does not use the 'callback' terminology for anything. If you want to redefine "callback" to be "any method to communicate between two piece of code" then you're free to, but we aren't talking about the same thing then and nobody else will know what you're talking about.


I'm arguing that the semantics aren't that different. Callbacks (such as used in Node and in libevent/libev-based async programs, as I said I don't really know gevent) and Erlang's process model both depend on individually dispatched messages to communicate events that are often fine-grained in nature: connection accepted, bytes received from buffer, failure occurred, child process died, name resolution completed, etc. This necessarily fragments the logic by requiring that each event be handled pieacemeal, either in separate callbacks or, in Erlang's case, often using pattern matching. I am arguing that the way you code these "callbacks" is superficially similar both in Erlang and in other async systems, and that the patterns, challenges, etc. are very similar, even though Erlang usually outcompetes the alternatives in terms of power and richness.


I really so very little similarity between how Erlang handles this communication and Node. Erlang processes communicate that events happen, sure. But resulting code has almost zero similarity to that of Node. Individual paths of execution don't even exist in Node since everything is jumbled together. Relating a callback to a pattern match is a rather naive comment.


You are still missing my point. Event-oriented code necessarily becomes non-linear and fragmented, with logic weaving in and out of event handlers, as supposed to classic synchronous, sequential code. This applies to any system based on "callbacks" — ie., asynchronous event handling — including Erlang.


No it doesn't. I can handle just those events that are relevant to the series of states I care about in order in Erlang. I can also toss them into another process. Interleaving is not required in Erlang.


Given how much callback based stuff exists in the Javascript world -- GUIs, Node.js and some recent W3C specs like the FileSystem API -- I thought it ought to be possible to write a simple sequencing function that can take care of this in the JS world. The key to a useful sequencing function is that it should let you customize the sequencing of the actions and recovery techniques pretty easily.

Here is my attempt at it - https://gist.github.com/2192413

Thoughts?


I'm waiting for the day when everybody realizes that callbacks and preemptive threading both suck, relative to automatic CPS transformations.

http://matt.might.net/articles/cps-conversion/


Slightly on-topic, a post of mine about how to have zero nested callbacks in Javascript: http://javascriptisawesome.blogspot.com/2012/03/zero-nested-...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: