Golang中如何理解goroutines的启动顺序

Golang中如何理解goroutines的启动顺序

package main

import "fmt"

func sum(a []int, c chan int) {
fmt.Println("ARRAY", a)
sum := 0
for _, v := range a {
	sum += v
}
 c <- sum // send sum to c
}

 func main() {
a := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)

go sum(a[len(a)/2:], c) //first
go sum(a[0:len(a)/2], c) // second
x, y := <-c, <-c // receive from c

fmt.Println(x, y, x+y)
}

在这个例子中,我们启动了两个goroutine,第一个(go sum(a[len(a)/2:], c))是包含值(-9, 4, 0)的int数组切片,第二个也是包含值(7, 2, 8)的int切片。我的问题是,虽然我们先启动了值为-5的goroutine,但为什么我们接收到的17比-5更快?另外,你能给我一些关于并发和通道的参考资料吗?


更多关于Golang中如何理解goroutines的启动顺序的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

是的

更多关于Golang中如何理解goroutines的启动顺序的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


非常感谢

那么如果我多次运行代码,输出可能会从 17 -5 12 变成 -5 17 12 吗?

尝试限制 runtime.GOMAXPROCS(1) => 2 并尝试理解发生了什么。虽然没有明确的顺序定义 😛

让我重申一下:goroutine 的执行没有固定顺序,它们只是被放入栈中,运行时在某个时刻决定哪个 goroutine 会被启动。即使有 100 个 goroutine,可能第 77 个 goroutine 会先执行。当我们想要从通道接收数据时,如果 100 个 goroutine 都准备好发送数据,运行时会随机选择一个 goroutine,而其他 goroutine 必须等待下一次发送数据的机会,我的理解正确吗?

没有定义的执行顺序——它们是并发运行的。哪个协程恰好先开始和/或先完成是随机的。

也许这样理解会有所帮助:不要将"go"关键字视为"启动"一个协程,而是看作创建一个协程数据结构并将其放入可运行协程池中。运行时调度器会在某个时刻查看这个池子并开始执行这些协程,必要时可能会创建新线程来运行它们。

两个通道发送者哪个会先完成也是非确定性的。假设你的协程立即启动并瞬间完成。当代码执行到第一个<-c时(在... := <-c, <-c中),会有两个协程尝试向通道发送数据。哪个被选中执行第一次写入是随机的,其结果会存入x。另一个则会服务于第二次读取,结果存入y

... := <-c, <-c

在Go语言中,goroutines的启动顺序并不保证执行完成顺序。当你使用go关键字启动goroutine时,这些goroutine会被调度到可用的操作系统线程上执行,但具体的执行时机由Go运行时调度器决定。

在你的代码示例中:

package main

import "fmt"

func sum(a []int, c chan int) {
    fmt.Println("ARRAY", a)
    sum := 0
    for _, v := range a {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}
    c := make(chan int)

    go sum(a[len(a)/2:], c) // 计算 [-9, 4, 0] 总和为 -5
    go sum(a[0:len(a)/2], c) // 计算 [7, 2, 8] 总和为 17
    
    x, y := <-c, <-c // 从通道接收值
    
    fmt.Println(x, y, x+y)
}

执行结果可能为:

ARRAY [-9 4 0]
ARRAY [7 2 8]
-5 17 12

或者:

ARRAY [7 2 8]
ARRAY [-9 4 0]
17 -5 12

关键点解释:

  1. goroutine调度不确定性:虽然你按顺序启动goroutines,但Go运行时调度器决定了它们的实际执行顺序。

  2. 通道接收顺序x, y := <-c, <-c 会按完成顺序接收值,而不是按启动顺序。

  3. 计算复杂度:计算 [7, 2, 8][-9, 4, 0] 的复杂度相同,但调度器可能让其中一个先完成。

更明确的示例:

package main

import (
    "fmt"
    "time"
)

func worker(id int, c chan int) {
    fmt.Printf("Worker %d started\n", id)
    time.Sleep(time.Duration(id) * 100 * time.Millisecond) // 模拟不同耗时
    c <- id
    fmt.Printf("Worker %d completed\n", id)
}

func main() {
    c := make(chan int)
    
    // 按顺序启动,但完成顺序取决于执行时间
    go worker(1, c) // 耗时较短
    go worker(3, c) // 耗时较长
    go worker(2, c) // 耗时中等
    
    // 接收顺序将反映完成顺序
    for i := 0; i < 3; i++ {
        result := <-c
        fmt.Printf("Received from worker %d\n", result)
    }
}

并发和通道参考资料:

Go官方文档:

关键概念代码示例:

// 缓冲通道示例
func bufferedChannelExample() {
    ch := make(chan int, 2) // 缓冲大小为2
    ch <- 1
    ch <- 2
    fmt.Println(<-ch) // 1
    fmt.Println(<-ch) // 2
}

// 使用select处理多个通道
func selectExample() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() { ch1 <- "from ch1" }()
    go func() { ch2 <- "from ch2" }()
    
    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

goroutines的执行顺序由Go运行时调度,无法保证先启动的goroutine会先完成,这是Go并发模型的设计特性。

回到顶部