

Coroutines are brittle and buggy - willvarfar
http://williamedwardscoder.tumblr.com/post/31725920676/explicitly-async-apis-vs-coroutines

======
klodolph
Coroutines are not the same thing as green threads, which the author seems to
half acknowledge but then really screws up the terminology for the rest of the
article. Green threads are things that can be implemented using coroutines, or
they can be implemented without coroutines.

Python generators are coroutines, a specific kind of coroutine. And you don't
see anyone calling them brittle or buggy.

The author's real complaint here is that the traditional shared-memory-plus-
locks model of concurrent programming is hard, and that's absolutely true. I
don't see how using a cooperative scheduler makes writing correct programs any
easier or harder.

------
redbad
Well, his example code demonstrating the "bug" is definitely broken. But not
because of any flaw in coroutines, rather in his understanding of them.

    
    
        session = sessions.get_session(cookie);
        if(!session)
            session = sessions.create_session(user_id);
    

Concurrent access to a global sessions object is obviously unsafe, even in a
coroutine environment where "You know [the above methods] don’t yield to the
scheduler." I'm not sure that anyone advocating for coroutines over async APIs
is arguing that coroutines somehow magically absolve the programmer from
considering race conditions. Only that coroutines, used idiomatically, remove
classes of bugs _like this one_. And that snippet is not idiomatic.

One "coroutine way" of handling concurrent access to shared data is by piping
requests to it through a synchronization point. For example, in pseudo-Go, it
may look like

    
    
        // public, synchronous API method
        func (s *Sessions) GetSession(cookie string) Session {
            // return s.dataStructure[cookie] // bad, obviously
            responseChannel := make(chan Session)
            s.requests <- getSessionRequest{cookie, responseChannel}
            return <-responseChannel
        }
    
        func (s *Sessions) loop() {
            for {
                select {
                 . . .
                case req := <-s.requests:
                    req.responseChannel <- s.dataStructure[req.cookie]
                 . . .
                }
            }
        }
    

(Another way is explicit locking, of course.)

~~~
StavrosK
Exactly. If you're relying on coroutines not yielding for your
synchronization, you're going to get bitten hard. Use per-coroutine data
structures and lock when accessing shared structures.

I tend to think about coroutines as threads, never relying on their
determinism, as that's not something you can count on, and it's not advertised
as one of their features.

------
DanWaterworth
This article has succeeded in convincing me that both ways are equally bad.

------
stevedekorte
His only complaint seems to be race conditions, but callbacks have these too.

In fact, coroutine based i/o use callbacks underneath just as while and for
loops use gotos (jump instructions) underneath.

The difference with both is that the abstraction allows your code to be more
understandable and easily changed.

For example, try making randomly selected locations in your code async using
callbacks - painfully difficult. Now try it with coroutines - easy.

There is a reason why we use stacks.

