Live data from Hacker News

Coroutines for Go

research.swtch.com

121–130 of 191 posts

Re: Coroutines for Go

#121
post #35

It looks like a lot of people are missing the point here. Yes a coroutine library would be a worse/more cumbersome way to do concurrency than the go keyword. The use case motivating all the complexity is function iterators, where `range` can be used on functions of type `func() (T, bool)`. That has been discussed in the Go community for a long time, and the semantics would be intuitive/obvious to most Go programmers.…

What is wrong with: for { next := getNext() ... } What is the advantage of writing this as: for next := range getNext { ... }

Well, what was wrong with:

    for {
        next := 

Re: Coroutines for Go

#122
post #35

Earlier quoted context omitted.

What is wrong with: for { next := getNext() ... } What is the advantage of writing this as: for next := range getNext { ... }

Well, what was wrong with: for { next :=

Horrible overhead. If the loop does something simple, like summing integers, 99% of time will be spent switching between goroutines.

From TFA:

"Because scheduling is explicit (without any preemption) and done entirely without the operating system, a coroutine switch takes at most around ten nanoseconds, usually even less. Startup and teardown is also much cheaper than threads."

"For this taxonomy, Go's goroutines are cheap threads: a goroutine switch is closer to a few hundred nanoseconds"

Also check out https://news.ycombinator.com/item?id=29510751

and https://ewencp.org/blog/golang-iterators/index.html:

"Finally, the most natural channel based implementation is… slow. By a factor of nearly 50x. Buffering does help though, reducing that to a factor of 25x."

Re: Coroutines for Go

#123

It looks like a lot of people are missing the point here. Yes a coroutine library would be a worse/more cumbersome way to do concurrency than the go keyword. The use case motivating all the complexity is function iterators, where `range` can be used on functions of type `func() (T, bool)`. That has been discussed in the Go community for a long time, and the semantics would be intuitive/obvious to most Go programmers.…

I think it's also worth mentioning that for certain specific use cases coroutines are much more efficient than a full goroutine, because switching to/from a coroutine doesn't require context switching or rescheduling anything. If you have two cooperating tasks that are logically synchronous anyway (e.g. an iterator) it's much more efficient to just run everything on the same CPU because the kernel doesn't have to res…

It's not just about performance, but also safety and ergonomics. Since true coroutines[1] offer predictable scheduling, their behavior with regards to data races and deadlocks is also more predictable.

If programmers try to manually implement iterators, generators and interleaved state machines with their own goroutines and channels, it's not just performance that suffers - there is too much room for error.

[1] I'm using the qualifier "true" here, since many modern languages (such as Python, Kotlin) use the term "coroutines" for something that is more like Go's Goroutines than Lua's coroutines. Unlike Go, they are not preemptible, but they are (at least by default) implicitly resumed when necessary by some scheduler, and they may execute on different kernel threads and switch contexts.

Re: Coroutines for Go

#124
post #117
post #2

Not sure I'm a fan. Looking through the examples, I feel like this makes the language much harder to read and follow, but maybe that's just my own brain and biases. Further, it doesn't seem to me to allow you to do anything you can't currently do with blocking channels and/or state.

What language change are you talking about? This is just a proposed construct to regularise and make efficient something people already do (as you says with “state”). I’ve used iterators similar to what’s described in this article to avoid allocations in critical code paths, but this would make those much less awkward to use (particularly with the upcoming range iterator language change).

Perhaps language change was bad wording, I guess I meant paradigm change encouraging? Just look at this func signature and first line...

> func Pull[V any](push func(yield func(V) bool)) (pull func() (V, bool), stop func()) {

> copush := func(more bool, yield func(V) bool) V {

The main power of Go to me was always quickly being able to read and understand code. This type of coding has a lot of cognitive load to a reader, I feel.

Re: Coroutines for Go

#125
This is a thoroughly interesting topic. Thanks for the article.

I haven't thought much about iterators link to coroutines.

As a hobby, I am working to write about a dream programming language. I happen to be really interested in parallelism, asynchronous, coroutines, multithreading and concurrency.

I want:

* seamlessly switch between remote-thread coroutine, local thread coroutine.

* concurrency and parallelism and async to be easy to think about, reason about, read and program

* programs should be easy to parallelise and be async and concurrent

Go iterators seem to be local to a thread, but what if you want to distribute work across threads?

I've been thinking of scheduling recently.

Imagine you're a search engine company and you want to index links between URLs. How would you solve this with coroutines?

  task download-url
   for url in urls:
    download(url)

  task extract-links
   parsed = parse(document)
   return parsed

  task fetch-links
   for link in document.query("a")
    return link

  task save-data
   db.save(url, link)

How would you do control flow and scheduling and parallelism and async efficiently with this code?

* `db.save()`, `download()` are IO intensive whereas `document.query("a")` and `parse` is CPU intensive.

* I want to handle plurality or multiple items trivially such as multiple URLs and multiple links.

* I want to keep IO and CPU in flight at all times.

I think I want this schedule:

https://user-images.githubusercontent.com/1983701/254083968-...

I have a toy 1:M:L 1 scheduler thread:M kernel threads:N lightweight threads lightweight scheduler in C, Rust and Java

https://github.com/samsquire/preemptible-thread

This lets me switch between tasks and preempt them from user space without assistance at descheduling time.

I have a simplistic async/await state machine thread pool in Java. My scheduling algorithm is very simple.

I want things like backpressure, circuit breakers, rate limiting, load shedding, rate adjustment, queuing.

Re: Coroutines for Go

#126
post #122

Earlier quoted context omitted.

Well, what was wrong with: for { next :=

Horrible overhead. If the loop does something simple, like summing integers, 99% of time will be spent switching between goroutines. From TFA: "Because scheduling is explicit (without any preemption) and done entirely without the operating system, a coroutine switch takes at most around ten nanoseconds, usually even less. Startup and teardown is also much cheaper than threads." "For this taxonomy, Go's goroutines are…

And even if it's a toy program and you don't care about performance, it's not as simple as just:

    for {
        next := 
You have to set up the channel and the goroutine that feeds it, you need to safely close the channel when the iteration is done (but not before, unless you like panics), you need to deal with panics inside the goroutine and possibly support cancellation if the iteration breaks early (unless you love memory leaks).

If you try to implement this pattern by hand, you are all too likely to make fatal mistake, and this is doubly true in the hands of an inexperienced programmer.

I appreciate the fact that Russ wrote this long post, gradually implementing `coro.New()` and improving its functionality and safety — and only in the very end, we get a short paragraph about performance. Good performance is important to make this feature attractive to use, but if the feature is clunky and error-prone, it wouldn't be worth much, even with great performance.

Re: Coroutines for Go

#127

Earlier quoted context omitted.

I think it's also worth mentioning that for certain specific use cases coroutines are much more efficient than a full goroutine, because switching to/from a coroutine doesn't require context switching or rescheduling anything. If you have two cooperating tasks that are logically synchronous anyway (e.g. an iterator) it's much more efficient to just run everything on the same CPU because the kernel doesn't have to res…

It's not just about performance, but also safety and ergonomics. Since true coroutines[1] offer predictable scheduling, their behavior with regards to data races and deadlocks is also more predictable. If programmers try to manually implement iterators, generators and interleaved state machines with their own goroutines and channels, it's not just performance that suffers - there is too much room for error. [1] I'm u…

> I'm using the qualifier "true" here, since many modern languages (such as Python, Kotlin) use the term "coroutines" for something that is more like Go's Goroutines than Lua's coroutines.

Python has both true (ish) coroutines (or at least coroutines which are entirely user controllable), which it mostly uses for iteration, and the concurrency specialised “async”.

Initially the goal was to reuse “yield” for concurrency (a big reason why “yield from” was added), but the ergonomics of mixing multiple coroutines uses was found to be awful, at least for the langage python is, and trying to provide relevant sugar difficult.

Re: Coroutines for Go

#128

Earlier quoted context omitted.

Looking at python's asyncio coroutine library, they are just mocking multithreading with asyncio.gather. Since coroutines can be executed in any order they are not really control-flow mechanisms. The selling point of coroutines over traditional threads is its lightweight but its moot since goroutines has similar memory cost to coroutines. The only real benefit is that coroutines are non blocking while goroutines may…

The concept of "coroutines" is as much about control flow as "subroutines" (function calls and return statements). But when you have a construct that has their own call stacks, it's a relatively small step to implement lightweight threads with it. Since doing concurrency happens much more often than any other smart use of coroutines, many people conflate the two. I sometimes see this confusion in discussions about Ko…

In fact python had general coroutines first in yield, which is mostly used for iteration (“generators”) and state machines.

Some frameworks (e.g. twisted) did use them for concurrency, and the core team originally planned something similar, however the ergonomics were not what they wanted (especially when mixing coroutines-for-concurrency and coroutines-for-iteration), so they went for a more specialised design.

Re: Coroutines for Go

#129
i've been thinking about a closely related feature in a different context: adding block arguments, as in smalltalk or ruby or especially lobster, to a language more like c, with static types and stack allocation

i think this would be favorable for (among other things) clu-like iterators and imgui libraries, where you often want to do something like

    submenu("&Edit") {
        command("&Cut") { clip_cut(getSelection()); }
        ...
    }
this is especially useful in a context where you're heap-allocating sparingly or not at all, because the subroutine taking the block argument can stack-allocate some resource, pass it to the block, and deallocate it once the block returns; python context managers and win32 paint messages are two cases where people commonly do this sort of thing, but things like save-excursion, with-output-file, transactional memory, and gsave/grestore also provide motivation

the conventional way to do this is to package up the block into a closure, then use a full-fledged function invocation to invoke it, using a calling convention that supports closures. but i suspect a more relaxed and efficient approach is to use an asymmetric coroutine calling convention, in which the callee yields back control to its caller at the entry point to the block, and the block then resumes the callee when it finishes. so instead of merely dividing registers into callee-saved and call-clobbered, as subroutine calling conventions do, we would divide them into callee-saved upon return but upon yield containing callee values the block must have restored upon resumption; caller coroutine context registers, which are callee-saved upon return and also on yield; and call-clobbered. you also need in many cases a way for the block to safely force an early exit from the callee

this allows the caller's local variables to be in registers its blocks can use without further ado, or at least indexed off of such a register, while allowing the yield and resume operations to be, in many cases, just a single machine instruction. and it does not require heap allocation

as an example of taking this to the point of absurdity, here's an untested subroutine for iterating over a nul-terminated string passed in r0 with a block passed in r1, using a hypothetical coroutine convention which passes at least r4 through from its caller to its blocks

    itersz: push {r6, r7, r8, lr}
            mov  r7, r0
            mov  r6, r1
    1:      ldrb r0, [r7], #1
            cbz  r0, 1f
            blx  r1
            b    1b
    1:      pop  {r6, r7, r8, pc}
and here is another untested subroutine which uses it to calculate a string hash

    hashsz: push {r4, r5, r9, lr}
            movs r4, #53
            adr  r1, 1f
            blx  itersz
            mov  r0, r4
            pop  {r4, r5, r9, pc}
    1:      eor  r4, r0, r4, ror #27
            bx   lr
even in this case where both the iteration and the visitor block are utterly trivial, the runtime overhead per item (compared to putting them in the same subroutine) is evidently extremely modest; my estimate is 7 cycles per byte rather than 4 cycles per byte on in-order hardware with simple branch prediction, so, on the order of 1 ns on the hardware russ used as his reference. for anything more complex the overhead should be insignificant

it's less general than the mechanism russ proposes here (it doesn't solve the celebrated samefringe problem), but it's also an order of magnitude more efficient, because the yield and resume operations are less work than a subroutine call, though still more work than, say, decrementing a register and jumping if nonzero

Re: Coroutines for Go

#130

Reading the comments makes me feel bittersweet. - Many people consider coroutines and green threads to be more or less the same thing, when they both have their pros and cons. - The fact that the omission of iterators is even acceptable in the Go community saddens me. They seem to deliberately refuse any feature that might make the language even slightly more complex, in the name of simplicity. But hey, at least they…

I wouldn’t confuse HN with “the Go community”. The head of the Go team wrote this post. Will the coro package proposed in the post be added exactly as written? Maybe, but probably not exactly as written. Will something like it be added? I would be willing to bet money on it, yes. How long will it take? I’d say at least a year (release in Go 1.23 in August of 2024), maybe a little longer. I don’t think it could be muc…

[deleted]
Post reply on HN