【问题标题】:Goroutines are cooperatively scheduled. Does that mean that goroutines that don't yield execution will cause goroutines to run one by one?Goroutines 是协作调度的。这是否意味着不让执行的 goroutine 会导致 goroutine 一个接一个地运行?
【发布时间】:2016-09-24 23:56:22
【问题描述】:

发件人:http://blog.nindalf.com/how-goroutines-work/

由于协程是协同调度的,一个连续循环的协程可能会饿死同一线程上的其他协程。

Goroutines 很便宜,如果它们被阻塞也不会导致多路复用的线程阻塞

  • 网络输入
  • 睡觉
  • 频道操作或
  • 阻止同步包中的原语。

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

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 会一个接一个地运行吗?

【问题讨论】:

  • 它是特定于实现的(也可能因版本而异),但不是。即使 goroutine 可以在执行过程中阻塞其他程序,也不能保证它们的调度顺序。
  • @JimB 啊好吧,所以他们没有任何定义的顺序,但他们会一个接一个地运行?
  • 此外,引用没有提到运行时的最新变化。 fmt.Println(sum) 可能会导致其他 goroutine 被调度,因为较新的运行时将在函数调用时调用调度程序。
  • 在实践中,它们可能会一个接一个地运行,但规范并没有对顺序做出任何保证,因此应该在假设执行顺序未定义的情况下编写代码。如果您想在 goroutine 之间进行协调,我建议使用通道来控制流程,即阻塞一个例程,直到另一个例程发出信号在通道上完成。
  • 文章说不能控制runtime创建的线程数;因此,将 runtime.GOMAXPROCS 设置为 1 并不能保证只有一个线程。 (根据我对这篇文章的理解,它通常是几个线程)。因此,您的问题似乎更具理论性并且很难测试。但非常有趣。好文章!

标签: multithreading go scheduling goroutine


【解决方案1】:

好吧,假设runtime.GOMAXPROCS 是 1。goroutines 一次同时运行一个。 Go 的调度器只是在一段时间内让一个派生的 goroutines 占上风,然后再让另一个 goroutines 占上风,等等,直到一切都完成。

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

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

因此,总体而言它们不是顺序的,即使一次只有一个 goroutine 处于活动状态 (GOMAXPROCS=1)。

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

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

我知道有一些我没有介绍的优点,但我不太了解并发性来告诉你。

编辑:

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

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

【讨论】:

  • 虽然让每个 goroutine 在让步给另一个 goroutine 之前运行一段时间,但这不是抢占式调度吗?我的印象是,合作安排某事时不会发生这种情况。
  • 它们是协作调度的,因为活动的 goroutine 将被执行而不是休眠的 goroutine。但是,调度取决于运行时 goroutine 的状态。 Goroutines 是绿色线程,这意味着 Go 正在模拟一个多线程环境,尽管在一个 proc 上运行时可能并非如此。基本上,它们比原生线程更轻。此外,由于在线程之间切换上下文而产生的开销存在于原生线程和绿色线程中。
  • @AR7,抢占式意味着内核(运行时)允许线程运行特定的时间量,然后在其他线程不做或不知道任何事情的情况下将执行权交给其他线程。在通常使用硬件中断实现的操作系统内核中。进程不能阻止整个操作系统。在协作多任务处理中,线程必须显式地让出执行给其他人。如果没有,它可能会阻塞整个过程甚至整个机器。这就是 Go 的做法。它有一些非常具体的点,goroutine 可以产生执行。但是如果 goroutine 只是执行for {} 那么它将锁定整个进程。
  • @AR7,正如我之前所说,您在那里有一个函数调用,可以在较新的 go 版本中执行。但是如果你没有任何函数调用,只是一些数学运算,那么是的,goroutine 将锁定线程,直到它退出或遇到可能产生执行给其他人的东西。这就是为什么 for {} 在 Go 中不起作用的原因。更糟糕的是,即使 GOMAXPROCS > 1,由于 GC 的工作原理,它仍然会导致进程挂起。但无论如何,你不应该依赖它。理解这些东西很好,但不要指望它。甚至有人提议在像你这样的循环中插入调度程序调用
  • @AR7,我们不知道,要看具体实现。它可能会切换,也可能不会,谁知道呢。 Go 的运行时所做的主要事情是它尽最大努力让每个人都可以执行并且不会饿死任何人。语言规范中没有指定它是如何做到的,并且将来可能会改变。如果关于循环的提议将被实施,那么即使没有函数调用切换也可能发生。目前你唯一应该记住的是,在某些情况下,函数调用可能会导致 goroutine 产生执行。
【解决方案2】:

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

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 {

    }
}

这会在某个随机点阻塞:

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

【讨论】:

  • 有趣......也许他们正在关闭,因为那里有一个打印,因为正如 creker 所说“此外,引用没有提到运行时的最新变化。fmt.Println(sum)可能会导致其他 goroutine 被调度,因为较新的运行时将在函数调用时调用调度程序”。我很好奇你是否可以设置一个没有打印语句的示例,它仍然可以以某种方式显示哪个 goroutine 正在执行。
  • @AR7,好点子。我也是这么想的,但是如果我将COUNT_TOPRINT_EVERY 减少100 倍,我就看不到切换了。所以我不认为他们会因为印刷而改变。
  • 那么这通过证明我错了来回答这个问题,但我希望得到的不仅仅是代码示例。你知道为什么他们关掉了吗?
  • @AR7,不幸的是,我没有。我在探索自己。如果我找到任何东西,我会告诉你的。
  • 就是这样。当fmt.Printf 被调用时,它首先会检查它是否需要增加堆栈并调用调度程序。它可能会切换到另一个 goroutine。是否会切换取决于其他 goroutine 的状态和调度器的具体实现。像任何调度程序一样,它可能会检查是否有饥饿的 goroutines 应该被执行。通过多次迭代,函数调用有更大的机会进行切换,因为其他人的饥饿时间更长。在饥饿发生之前,goroutine 只需少量迭代即可完成。
【解决方案3】:

Creker 的所有 cmets 的汇编和整理。

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

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

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

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

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

【讨论】:

    【解决方案4】:

    @Akavall 在创建哑 goroutine 后尝试添加睡眠,goruntime 永远不会执行 sum_up goroutines。

    看起来 go runtime 会立即生成下一个 goroutine,它可能会执行 sum_up goroutine 直到 go runtime 安排dumb() goroutine 运行。一旦dumb() 被安排运行,go runtime 将不会安排 sum_up goroutines 运行,因为dumb 运行{}

    【讨论】:

      猜你喜欢
      • 2018-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-26
      • 2013-02-05
      相关资源
      最近更新 更多