Go语言 goroutine

下面的代码,在for里面调用函数前加上了go之后,使func进行并发执行。

package main

import (
	"fmt"
	"time"
)

func main() {
	for i := 0; i < 1000; i++ {
		go func(i int) {
			for {
				fmt.Printf("Hello from "+
					"goroutine %d\n", i)
			}
		}(i)
	}
	time.Sleep(time.Minute)
}
  • 上面的代码如果不加go执行的话,只会在第一次i=0的时候无限循环执行fmt.Printf("Hello from "+"goroutine %d\n", i)

  • 如果加上go,但是不加time.Sleep(time.Minute)的话,在func执行之前,for文就会结束,导致内部的func来不及执行。

go开启的其实不是线程,而是协程。

  • 轻量级线程

  • 非抢占式多任务处理,由协程主动交出控制权

  • 编译器/解释器/虚拟机层面的多任务(线程是系统级)

  • 多个协程可能在一个或者多个线程上运行

非抢占式

关于非抢占式可以在看一个例子。

func main() {
	var a [10]int
	for i := 0; i < 10; i++ {
		go func(i int) {
			for {
				a[i]++
			}
		}(i)
	}
	time.Sleep(time.Minute)
	fmt.Println(a)
}

这段代码执行以后我们会发现程序死机了。

主要原因有2个

  • a[i]++无法主动交出控制权

  • main函数自身也是一个协程,但是time.Sleep(time.Minute)因为没有人交出控制权,导致main一直等待。

从之前的代码可以看到,io操作时可以自动交出控制权的,如何手动交出控制权?可以使用runtime.Gosched()一般情况下不会用到runtime.Gosched()

func main() {
	var a [10]int
	for i := 0; i < 10; i++ {
		go func(i int) {
			for {
				a[i]++
                runtime.Gosched()
			}
		}(i)
	}
	time.Sleep(time.Minute)
	fmt.Println(a)
}

关于闭包

还有一个点,可以看到for的里面的函数func定义了参数i,每次调用的时候都传入了i。理论上我们应该直接用i的。

如果我们去掉传参的处理执行的话发现会报错。

func main() {
	var a [10]int
	for i := 0; i < 10; i++ {
		go func() {
			for {
				a[i]++
				runtime.Gosched()
			}
		}()
	}
	time.Sleep(time.Millisecond)
	fmt.Println(a)
}

这个时候我们可以用go run -race goroutine.go来看一下发生了什么。

==================
WARNING: DATA RACE
Read at 0x00c00008c008 by goroutine 6:
  main.main.func1()
      /Users/ding/go/coding-180/lang/goroutine/goroutine.go:16 +0x70

Previous write at 0x00c00008c008 by main goroutine:
  main.main()
      /Users/ding/go/coding-180/lang/goroutine/goroutine.go:11 +0x11b

Goroutine 6 (running) created at:
  main.main()
      /Users/ding/go/coding-180/lang/goroutine/goroutine.go:12 +0xf1
==================

出错的原因是因为如果不把i作为参数传给func,就不会形成闭包,在最后一次执行for的时候i会是10,导致a[10]超出范围。你也可以把var a [10]int改成var a [11]int这样就不会报错了,但是这很不安全,所以不建议这样做。

协程和普通函数

其他语言的协程支持

go语言的调度器

在go语言里,具体每个协程如何分配到线程里,都是交给调度器来分配,我们不用操心。

goroutine的定义

  • 直接在函数前加入go

  • 不需要再定义时区分是否是异步函数

  • 调度器在合适的点进行切换

  • 使用-race检测数据访问冲突

goroutine的切换点

下面这些点可以参考,但是由于我们无法100%控制,所以只能是参考。

  • I/O select

  • channel

  • 等待锁

  • 函数调用(有时)

  • runtime.Gosched()

Last updated