Goroutines are cooperatively scheduled. Does that mean that goroutines that don't yield execution will cause goroutines to run one by one?
Asked Answered
P

4

20

From: http://blog.nindalf.com/how-goroutines-work/

As the goroutines are scheduled cooperatively, a goroutine that loops continuously can starve other goroutines on the same thread.

Goroutines are cheap and do not cause the thread on which they are multiplexed to block if they are blocked on

  • network input
  • sleeping
  • channel operations or
  • blocking on primitives in the sync package.

So given the above, say that you have some code like this that does nothing but loop a random number of times and print the sum:

func sum(x int) {
  sum := 0
  for i := 0; i < x; i++ {
    sum += i
  }
  fmt.Println(sum)
}

if you use goroutines like

go sum(100)
go sum(200)
go sum(300)
go sum(400)

will the goroutines run one by one if you only have one thread?

Publicness answered 26/5, 2016 at 20:2 Comment(5)
It's implementation specific (and can vary between versions too), but no. Even if though goroutine can block the others during its execution, the order in which they are scheduled isn't guaranteed.Skiascope
@Skiascope ah ok so they don't have any defined order, but they will run one after another?Publicness
Futhermore, the quote doesn't mention recent changes in the runtime. fmt.Println(sum) could cause other goroutines to be scheduled as newer runtimes will call scheduler on function calls.Affenpinscher
In practice they'd probably run one after another but the spec doesn't make any guarantees about order so code should be written with the assumption that order of execution is undefined. If you want coordination between goroutines I recommend using channels to control the flow ie blocking one routine until another signals it's completion on a channel.Circumstantial
The article says that you cannot control number of threads that runtime creates; therefore, setting runtime.GOMAXPROCS to 1 will not guarantee just one thread. (From how I understand the article it will usually be few threads). So it seems that your question is more theoretical and very hard to test. But very interesting. Good Article!Chemo
P
20

A compilation and tidying of all of creker's comments.

Preemptive means that kernel (runtime) allows threads to run for a specific amount of time and then yields execution to other threads without them doing or knowing anything. In OS kernels that's usually implemented using hardware interrupts. Process can't block entire OS. In cooperative multitasking thread have to explicitly yield execution to others. If it doesn't it could block whole process or even whole machine. That's how Go does it. It has some very specific points where goroutine can yield execution. But if goroutine just executes for {} then it will lock entire process.

However, the quote doesn't mention recent changes in the runtime. fmt.Println(sum) could cause other goroutines to be scheduled as newer runtimes will call scheduler on function calls.

If you don't have any function calls, just some math, then yes, goroutine will lock the thread until it exits or hits something that could yield execution to others. That's why for {} doesn't work in Go. Even worse, it will still lead to process hanging even if GOMAXPROCS > 1 because of how GC works, but in any case you shouldn't depend on that. It's good to understand that stuff but don't count on it. There is even a proposal to insert scheduler calls in loops like yours

The main thing that Go's runtime does is it gives its best to allow everyone to execute and don't starve anyone. How it does that is not specified in the language specification and might change in the future. If the proposal about loops will be implemented then even without function calls switching could occur. At the moment the only thing you should remember is that in some circumstances function calls could cause goroutine to yield execution.

To explain the switching in Akavall's answer, when fmt.Printf is called, the first thing it does is checks whether it needs to grow the stack and calls the scheduler. It MIGHT switch to another goroutine. Whether it will switch depends on the state of other goroutines and exact implementation of the scheduler. Like any scheduler, it probably checks whether there're starving goroutines that should be executed instead. With many iterations function call has greater chance to make a switch because others are starving longer. With few iterations goroutine finishes before starvation happens.

Publicness answered 26/5, 2016 at 20:3 Comment(1)
Are we saying that at thread level, it is preemptive scheduling like threads get kicked out of processor based on time. But within a thread, the goroutines are scheduled with cooperative scheduling?Tumescent
C
4

For what its worth it. I can produce a simple example where it is clear that the goroutines are not ran one by one:

package main

import (
    "fmt"
    "runtime"
)

func sum_up(name string, count_to int, print_every int, done chan bool) {
    my_sum := 0
    for i := 0; i < count_to; i++ {
        if i % print_every == 0 {
            fmt.Printf("%s working on: %d\n", name, i)
        }
        my_sum += 1
    }
    fmt.Printf("%s: %d\n", name, my_sum)
    done <- true 
}

func main() {
    runtime.GOMAXPROCS(1)
    done := make(chan bool)

    const COUNT_TO =   10000000
    const PRINT_EVERY = 1000000

    go sum_up("Amy", COUNT_TO, PRINT_EVERY, done)
    go sum_up("Brian", COUNT_TO, PRINT_EVERY, done)

    <- done 
    <- done 

}

Result:

....
Amy working on: 7000000
Brian working on: 8000000
Amy working on: 8000000
Amy working on: 9000000
Brian working on: 9000000
Brian: 10000000
Amy: 10000000

Also if I add a function that just does a forever loop, that will block the entire process.

func dumb() {
    for {

    }
}

This blocks at some random point:

go dumb()
go sum_up("Amy", COUNT_TO, PRINT_EVERY, done)
go sum_up("Brian", COUNT_TO, PRINT_EVERY, done)
Chemo answered 28/5, 2016 at 1:53 Comment(5)
Interesting...perhaps they're switching off because there is a print in there because as creker said "Futhermore, the quote doesn't mention recent changes in the runtime. fmt.Println(sum) could cause other goroutines to be scheduled as newer runtimes will call scheduler on function calls". I'm curious if you could set up an example without print statements that still somehow could show which goroutine is executing when.Publicness
@AR7, Good point. I thought that too, but if I reduce COUNT_TO and PRINT_EVERY by factor of 100, I don't see the switching anymore. So I don't think they are switching because of the print.Chemo
Then this answers the question by demonstrating I am wrong, but I was hoping to get more than a code example. Do you have any idea why they switch off?Publicness
@AR7, Unfortunately, I do not. I was exploring myself. I will let you know if I find anything.Chemo
That's exactly what happens. When fmt.Printf is called first thing it does is checks whether it needs to grow the stack and calls the scheduler. It MIGHT switch to another goroutine. Whether it will switch depends on the state of other goroutines and exact implementation of the scheduler. Like any scheduler, it probably checks whether there're starving goroutines that should be executed instead. With many iterations function call has greater chance to make a switch because others are starving longer. With few iterations goroutine finishes before starvation happens.Affenpinscher
T
2

Well, let's say runtime.GOMAXPROCS is 1. The goroutines run concurrently one at a time. Go's scheduler just gives the upper hand to one of the spawned goroutines for a certain time, then to another, etc until all are finished.

So, you never know which goroutine is running at a given time, that's why you need to synchronize your variables. From your example, it's unlikely that sum(100) will run fully, then sum(200) will run fully, etc

The most probable is that one goroutine will do some iterations, then another will do some, then another again etc.

So, the overall is that they are not sequential, even if there is only one goroutine active at a time (GOMAXPROCS=1).

So, what's the advantage of using goroutines ? Plenty. It means that you can just do an operation in a goroutine because it is not crucial and continue the main program. Imagine an HTTP webserver. Treating each request in a goroutine is convenient because you do not have to care about queueing them and run them sequentially: you let Go's scheduler do the job.

Plus, sometimes goroutines are inactive, because you called time.Sleep, or they are waiting for an event, like receiving something for a channel. Go can see this and just executes other goroutines while some are in those idle states.

I know there are a handful of advantages I didn't present, but I don't know concurrency that much to tell you about them.

EDIT:

Related to your example code, if you add each iteration at the end of a channel, run that on one processor and print the content of the channel, you'll see that there is no context switching between goroutines: Each one runs sequentially after another one is done.

However, it is not a general rule and is not specified in the language. So, you should not rely on these results for drawing general conclusions.

Triplane answered 26/5, 2016 at 20:21 Comment(7)
Isn't that preemptive scheduling though to have each goroutine run for a bit before yielding to another? I was under the impression that this isn't what happens when something is cooperatively scheduled.Publicness
They are cooperatively scheduled in the sense that active goroutines will be executed instead of sleeping ones. However, the schedule depends on the state of goroutines at runtime. Goroutines are green threads, that means Go is emulating a multi-threaded environment, though it may not be the case when running on one proc. Basically, they are lighter than native threads. Also, the overhead due to switching context between threads is present in both native and green threads.Triplane
@AR7, preemptive means that kernel (runtime) allows threads to run for a specific amount of time and then yields execution to other threads without them doing or knowing anything. In OS kernels that's usually implemented using hardware interrupts. Process can't block entire OS. In cooperative multitasking thread have to explicitly yield execution to others. If it doesn't it could block whole process or even whole machine. That's how Go does it. It has some very specific points where goroutine can yield execution. But if goroutine just executes for {} then it will lock entire process.Affenpinscher
@Affenpinscher right so in this case where it's on a single thread, it's just a single for loop, and doesn't have any I/O blocking it or anything that should cause it to yield execution, each goroutine should lock up the other goroutines until the for loop finishes, meaning they would run one after another instead of switching, correct?Publicness
@AR7, as I said earlier, you have a function call there which could yield execution in newer go versions. But if you don't have any function calls, just some math, then yes, goroutine will lock the thread until it exits or hits something that could yield execition to others. That's why for {} doesn't work in Go. Even worse, it will still lead to process hanging even if GOMAXPROCS > 1 because of how GC works. But in any case you shouldn't depend on that. It's good to understand that stuff but don't count on it. There is even a proposal to insert scheduler calls in loops like yoursAffenpinscher
@Affenpinscher Yeah I just hope to understand it conceptually not abuse it or anything. Although one last thing I'm wondering is that since the function call comes at the very end, after the for loop, does it even matter? I could see goroutines switching if the print statement was inside the for loop since that's some form of IO during the execution of the for loop, but if it's outside of the for loop it shouldn't cause any switching because at that point it'll just exit anyways right?Publicness
@AR7, we don't know, it depends on the specific implementation. It might switch or might not, who knows. The main thing that Go's runtime does is it gives its best to allow everyone to execute and don't starve anyone. How it does that is not specified in the language specification and might change in the future. If the proposal about loops will be impemented then even without function calls switching could occur. At the moment the only thing you should remember is that in some circumstances function calls could cause goroutine to yield execution.Affenpinscher
O
0

@Akavall Try adding sleep after creating dumb goroutine, goruntime never executes sum_up goroutines.

From that it looks like go runtime spawns next go routines immediately, it might execute sum_up goroutine until go runtime schedules dumb() goroutine to run. Once dumb() is scheduled to run then go runtime won't schedule sum_up goroutines to run, as dumb runs for{}

Ounce answered 23/4, 2020 at 15:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.