--
---
You received this message because you are subscribed to the Google Groups "golang-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-dev+...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
As I understand it, a negative channel cap only differs from an unbuffered chan in that the negative cap would hint to (a modified) current runtime that which a future compiler or runtime may be able to determine automatically. In that sense, it feels like the C++ strategy to solving performance problems.Furthermore, needing to support an unbuffered fallback at runtime at times when more than two goroutines are involved means that we may not be able to take advantage of certain performant assumptions. If Dmitry's suggestion were accepted, it seems reasonable enough to panic if these special-cased channels do not fall within expected/assumed usage patterns. Alternatively, we could require the input code to fall within a given flow structure, and abort compilation if it does not (similarly to how we handled return statements: as we understand the problem better, we can expand allowed use-cases).
Arguably, for-range loops comprise most of the use cases -- if we can optimize around that case, we may be able to achieve a 90/10 solution. Unfortunately, emulating generators with early termination requires either a quit chan, a semantic change, or codifying existing bad behavior into a convention: for example, it's currently bad behavior for a consumer to close a channel, though if that were codified into a generator idiom, then something like the following could be used:func producer(i int) <-chan int {ch := make(chan int, -1)go func() {defer func() { recover() }()for {ch <- ii++}}()return ch}
func consumer() {ch := producer(0)for i := range ch {fmt.Println(i)if i > 50 {close(ch)}}}With a few modifications, this works already: http://play.golang.org/p/7t4oT6OqUJStill, this is much more setup than one would want if generators were to idiomatic. In particular, chan creation (since for a synchronous generator it would be unbuffered anyway), and explicit closing or signalling of early termination is burdensome. Ideally, you'd want something like:func consumer() {// gen could be a builtin that takes a function call expression returning a single// named <-chan, makes that chan, and produces a sentinel'd panic in producer when// the following loop is exited. The runtime will implicitly catch the sentinel,// preventing program termination.for i := range gen(producer(0)) {fmt.Println(i)if i > 50 {break}}}func producer(i int) (ch <-chan int) {for {ch <- ii++}}
It's a bit more magic than I think any of us would like, though if generators are ever going to become first-class, I believe code volume will need to kept about as minimal as demonstrated in the above example -- whether or not something like the above syntax is used, the current minimum code required involves too much "boilerplate" to make it non-tedious for frequent use. I think the most ideal (and simplest) semantics would be: the producer does not execute any instructions until a receive on its channel has occurred, and then it only executes until its next send; at no time are both the consumer and producer simultaneously executing (this is, for example, how python treats generators). This doesn't address multiple consumers, though those use cases tend to complicate things enough that the current syntax and semantics are usually already ideal.The comment text above is in terms of familiar concurrent go semantics, though an efficient implementation may not actually produce a separate goroutine (or would at least treat the pair of goroutines as a combined scheduling entity).--------------------------------Moving past the above... Assuming current syntax and semantics are retained (rather than extended), it seems that using channels as the primary unit of analysis is problematic: just because a channel does not escape a set of two goroutines does not mean that those goroutines do not communicate across other channels as well. Transitive goroutine relationships may be a better unit of analysis -- if it can be determined that all channel use within a given stretch of code are such that the graph of communicated-with goroutines is guaranteed to flow along at most one edge at a time (which probably implies that all channels are unbuffered), then those goroutines can all be statically scheduled together. `6g -m` could be made to also indicate whether or not any concurrent goroutines were determined to form an isolated mutually-exclusive-execution network.In terms of potential optimizations, all chans in the graph could be combined into a compact memory structure along the lines of:struct coroutineChanSet {int chanId;union {// union of channel types} chanVal;};(since analysis would have already guaranteed one channel in use at a time, a tagged union could service all channels). Signalling wouldn't be needed since jmp's could be used to switch goroutines. If there's reason not to inline the goroutines (they're too big and are used in multiple contexts), it may be possible to generate a wrapping trampoline at compile time for each group of related goroutines that could transparently turn channel ops into some stack ops and a jmp. Of course, all this is just theory -- implementation seems like it'd be extremely difficult (non-escaping []byte <-> string optimization, for example, would probably involve far less effort yet have greater net benefit). Also, the set of scenarios in which the semantics are blatantly obvious is fairly limited: goroutines that assign nil to their channels in the middle of a select loop, for example, might not fit into this model.
IMO, it seems better to wait until we can make the compiler figure all this out (even if that never happens) rather than changing the language to have this today.
--
On Sunday, April 14, 2013 6:05:24 PM UTC-6, Dmitry Vyukov wrote:
This is very close to the previous proposal for:vals := make(<-chan int, func(ch chan<- int) {for i := 0; i < 100; i++ {ch <- i}}())right?I would actually prefer the make(chan int, producer) form (over gen(producer) form), because it makes it clear that the construct returns a chan. However, I think the exact syntax does not matter that much on this stage.
I wasn't actually intending to make a language proposal -- just demonstrating approximately the amount of code volume we'd need to get rid of this for practical use. The make proposal certainly fulfills that need, and is much less magical.There are still some additional deadlock possible, e.g. the following code will deadlock...because the producer gooroutine will be blocked on c1 send, while the consumer waits for a value on c2.That's opposite to the behavior I'd expect (since just about anything with more than one channel outside of select, even when reading and writing in a consistent order, which is generally expect to avoid deadlock, will deadlock). I see why from an implementation perspective you'd want "blocks until *next* request" so as to force a something like lock-step execution behavior without requiring the core scheduler to be aware of what you're doing. However, this still wouldn't prevent the producer/generator from running independently until it reaches the first non-parallel channel op.
Making the scheduler aware of this execution flow would allow it to prevent simultaneous execution without the restrictive semantics.The make-generator proposal, for example, could mark the implicitly launched goroutine as being execution-constrained to the given channel: the goroutine does not get scheduled until there is a blocking receive waiting on that channel
(so a select on that channel is never ready). Once such a recv operation begins, the recv'ing G blocks until the producing G is unscheduled (which in the case of 1 producer and 1 consumer, would be immediate); the producer G is then scheduled onto the same M that the consuming G was running on. Without a pre-emptive scheduler, this may be what happens already (at least for the initial iteration),
except that in this case the running G would be guaranteed to be unscheduled as soon as it crosses the communication boundary of the specially made channel.
Hello, I started discussing coroutines with Dmitry a few weeks ago. Dmitry told me last week he hadmoved the discussion to golang-dev with some ideas on the topic. I've just managed to read throughall the messages.It might be helpful to give my original points about coroutines that started the discussion, becausethe discussion in golang-dev started in the middle with an implementation proposal for an unknownmechanism, which people later deduced to be coroutines.I want to start with two primitive execution mechanisms: thread and stack. A thread causes action ina program and a stack is the mechanism used by a thread to perform sequential execution usingroutines.It is possible to draw a table of how these two basic mechanism are combined to generate programminglanguage constructs:stack| no | yes-------------+--------------+-------------no | 1. routine | 2. coroutinethread --+--------------+-------------yes | 3. ??? | 4. goroutine1. routine : use caller's thread and stack2. coroutine : use caller's thread and coroutine's stack3. ??? : use thread but no stack (difficult to imagine)4. goroutine : use goroutine's thread and stackWhat's nice about this table is that it suggests language constructs based on fundamentalmechanisms. Not all combinations make sense, but ones that make sense are often difficult orimpossible to implement with the others. Go has language constructs for 1 and 4 but not 2.Coroutines are a powerful extension to sequential programming, i.e., the ability to retain BOTH dataand execution state between calls. Closures can retain data state between calls but not executionstate. Retaining execution state significantly simplifies writing call-backs, device-drives, andother mechanisms in event-driven programming. And even with low-cost user-level threads,event-driven programming is still useful and necessary in many situations. Finally, it is a racefor two threads to simultaneously execute a coroutine as the stack is shared, so simultaneousexecution is undefined as for other unsafe shared accesses. Threads can take turns executing acoroutine, but this kind of sharing is infrequent.Coroutines are popular now in a number of languages, such as Python's "yield". Coroutines can bestackless (actually single frame) or stackfull. For example, coroutines in Python can yield andretain state between the yields but only in the single frame containing the "yield"; a stacklesscoroutine cannot call out and have one of the called routines yield. A stackfull coroutine can callout and yield at any point, which requires a full stack.So I have tried to give some justification for having coroutines in a programming language; if youdon't agree with these reasons, you can stop reading now.Since Go does not have coroutines there are 3 options:1. simulation using existing language mechanisms2. extend languagea) using an existing mechanism, like channelsb) using new constructs1. Simulation=============If a language can simulate a mechanism with reasonable syntax and performance, that is the preferredpath.Crucial point: coroutines are sequential. Therefore, a simulation cannot display any form ofconcurrency, which would generate non-program-order behaviour.Attached is my sample simulations, which illustrate the issues. The larger questions is can thefinal and most general simulation be packaged in a simpler form that is easier for programmers?I used the simulation approach to implement a few attached example coroutine programs.2. Extend Language==================a) Dmitry and others showed how to extend the channel mechanism to achieve a better simulation, withpossible additional functionality.b) The final approach is to add new language constructs, e.g.:co wholes func() int { // create coroutinecount := 0for {yield countcount += 1fmt.Println("X")}}()for n := 0; n < 10; n += 1 {fmt.Println(wholes())}This CLEARLY needs more thought, so don't jump on me. However, a new-construct approach shouldgive the best performance because it is a direct context switch between coroutines without havingto play with channels and/or perform a Go-runtime schedule. Finally, there are no special caseswith respect to channels. The thread of the goroutine executing the coroutine is the onemanipulating the channel so no new semantics are required; the goroutine thread is just executingon a different stack.
It is possible to draw a table of how these two basic mechanism are combined to generate programminglanguage constructs:stack| no | yes-------------+--------------+-------------no | 1. routine | 2. coroutinethread --+--------------+-------------yes | 3. ??? | 4. goroutine1. routine : use caller's thread and stack2. coroutine : use caller's thread and coroutine's stack3. ??? : use thread but no stack (difficult to imagine)4. goroutine : use goroutine's thread and stack