Goroutines 是协作调度的。这是否意味着不让出执行的 goroutines 将导致 goroutines 一个一个 运行 ?

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

发件人: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.

鉴于上述情况,假设您有这样的代码,除了循环随机次数并打印总和外什么都不做:

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

如果你使用像

这样的goroutines
go sum(100)
go sum(200)
go sum(300)
go sum(400)

如果你只有一个线程,goroutines 运行 会一个一个吗?

好吧,假设 runtime.GOMAXPROCS 是 1。goroutines 运行 一次并发一个。 Go 的调度程序只是在一段时间内让一个生成的 goroutine 占上风,然后再给另一个 goroutine,依此类推,直到所有 goroutine 都完成。

所以,你永远不知道哪个 goroutine 在给定时间 运行ning,这就是你需要同步变量的原因。从你的例子来看,sum(100) 不太可能 运行 完全,然后 sum(200) 将 运行 完全,等等

最有可能的是一个 goroutine 会做一些迭代,然后另一个 goroutine 会做一些,然后再另一个 goroutine 等等。

所以,总的来说它们不是顺序的,即使一次只有一个 goroutine 处于活动状态 (GOMAXPROCS=1)。

那么,使用 goroutines 有什么好处呢?很多。这意味着您可以只在 goroutine 中执行一个操作,因为它并不重要并继续主程序。想象一个 HTTP 网络服务器。在 goroutine 中处理每个请求很方便,因为您不必关心它们的排队和 运行 它们的顺序:您让 Go 的调度程序完成工作。

另外,有时 goroutines 是不活跃的,因为你调用了 time.Sleep,或者它们正在等待一个事件,比如接收一个频道的东西。 Go 可以看到这一点,并在一些处于空闲状态时执行其他 goroutines。

我知道有一些优点我没有介绍,但我不太了解并发性,无法向您介绍它们。

编辑:

与您的示例代码相关,如果您在通道的末尾添加每次迭代,运行 在一个处理器上打印通道的内容,您会看到没有上下文切换goroutines 之间:每个 运行s 依次完成另一个 goroutines。

但是,它不是一般规则,未在语言中指定。因此,您不应依赖这些结果来得出一般性结论。

物有所值。我可以举一个简单的例子,很明显 goroutines 不是 运行 一个接一个:

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 

}

结果:

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

此外,如果我添加一个只执行永远循环的函数,那将阻止整个过程。

func dumb() {
    for {

    }
}

这会在某些 运行dom 点阻塞:

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

所有 creker 评论的汇编和整理。

Preemptive 意味着内核 (运行time) 允许线程 运行 一段特定的时间,然后在其他线程不做任何事情或不知道任何事情的情况下将执行权交给其他线程。在通常使用硬件中断实现的 OS 内核中。进程无法阻止整个 OS。在协作式多任务线程中,线程必须显式地将执行权让给其他线程。如果不这样做,它可能会阻止整个过程甚至整个机器。 Go 就是这样做的。它有一些非常具体的点,goroutine 可以让出执行。但是如果 goroutine 只是为 {} 执行那么它会锁定整个进程。

但是,引用中没有提到 运行 时间的最新变化。 fmt.Println(sum) 可能会导致其他 goroutine 被调度,因为较新的 运行 次将在函数调用时调用调度程序。

如果您没有任何函数调用,只是一些数学运算,那么是的,goroutine 将锁定线程,直到它退出或遇到可以让其他人执行的东西。这就是 for {} 在 Go 中不起作用的原因。更糟糕的是,由于 GC 的工作原理,即使 GOMAXPROCS > 1,它仍然会导致进程挂起,但无论如何你不应该依赖它。了解这些东西很好,但不要指望它。甚至有人建议在像你这样的循环中插入调度程序调用

Go 的 运行time 所做的主要事情是它尽最大努力让每个人都能执行并且不会饿死任何人。它是如何做到的,语言规范中没有具体说明,将来可能会改变。如果关于循环的提议得到实施,那么即使没有函数调用切换也可能发生。目前你唯一应该记住的是,在某些情况下函数调用可能会导致 goroutine 放弃执行。

为了解释 Akavall 的答案中的切换,当调用 fmt.Printf 时,它所做的第一件事是检查是否需要增加堆栈并调用调度程序。它可能会切换到另一个 goroutine。它是否会切换取决于其他 goroutine 的状态和调度程序的具体实现。像任何调度程序一样,它可能会检查是否有应该执行的饥饿 goroutines。通过多次迭代,函数调用有更大的机会进行切换,因为其他人的饥饿时间更长。在饥饿发生之前,goroutine 完成了几次迭代。

@Akavall 尝试在创建 dumb goroutine 后添加 sleep,go运行time 永远不会执行 sum_up goroutines.

由此看来 go 运行time 会立即生成下一个 go 例程,它可能会执行 sum_up goroutine 直到 go 运行time schedules dumb() goroutine to 运行.一旦 dumb() 被调度到 运行 然后 go 运行time 将不会调度 sum_up goroutines 到 运行,因为 dumb 运行s for{}