本文发表于 683 天前,其中的信息可能已经事过境迁

初识Goroutine

Goroutine是Go语言的一个重要特性,是Go语言并发编程的基础。Goroutine是Go语言的轻量级线程,由Go语言运行时管理,可以轻松实现并发编程,与传统的线程相比,。Goroutine的调度由Go语言运行时管理,Go语言运行时会自动将Goroutine分配到多个操作系统线程上,实现并发执行。Go语言运行时会根据系统的负载情况自动调整Goroutine的数量,以实现最佳的性能。

Go并发实现原理

Go实现了两种并发形式,一种是CSP(Communicating Sequential Processes)并发模型,另一种是多线程共享内存模型。CSP并发模型是Go语言的并发编程模型,是Go语言并发编程的基础,通过通道(Channel)实现Goroutine之间的通信。多线程共享内存模型是传统的并发编程模型,通过共享内存实现线程之间的通信,Go语言也支持多线程共享内存模型,但是Go语言推荐使用CSP并发模型,因为CSP并发模型更安全、更高效、更易于理解。

在使用CSP模型时时刻牢记:

不要通过共享内存来通信,而应该通过通信来共享内存

在下一章Go学习笔记将会详细介绍为什么要这样做,以及如何实现。

Goroutine上手

Goroutine的创建

Goroutine的创建非常简单,只需要在函数调用前加上go关键字即可,如下所示:

go
go func() {
    fmt.Println("Hello, Goroutine!")
    // go func()函数体
}()

基础示例

基本用法:

go
// Goroutine 外部函数
package main

import (
    "fmt"
    "time"
)

func PrintHello() {
    fmt.Println("Hello, Goroutine!")
}

func main() {
    go PrintHello()
    time.Sleep(time.Second)
}
go
// Goroutine 内部匿名函数
package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        fmt.Println("Hello, Goroutine!")
    }()
    time.Sleep(time.Second)
}

在上面的代码中,我们在main函数中创建了一个Goroutine,Goroutine的函数体为fmt.Println("Hello, Goroutine!"),在main函数中调用time.Sleep(time.Second)函数,让程序等待1秒钟,以保证Goroutine有足够的时间执行。运行上面的代码,可以看到程序输出了Hello, Goroutine!,说明Goroutine成功执行。其实,main函数也是一个Goroutine,Go语言程序的入口函数main函数会被自动创建为一个Goroutine,称为主Goroutine。

下面,来进行一些实际操作吧,我们看看Goroutine的执行效果:

go
package main

import (
    "fmt"
    "time"
)

func PrintHello() {
    fmt.Println("Hello, Goroutine!")
}

func main() {
    go PrintHello()
    fmt.Println("Hello, main!")
}

此时,输出结果为:

shell
Hello, main!

如果你是初学者,你肯定好奇为什么没有输出Hello, Goroutine!,这是因为PrintHello函数相当于异步执行了,main函数执行完毕后,程序就退出了,Goroutine没有足够的时间执行。为了让Goroutine有足够的时间执行,我们可以在main函数中调用time.Sleep(time.Second)函数,让程序等待1秒钟,以保证Goroutine有足够的时间执行。

go
package main

import (
    "fmt"
    "time"
)

func PrintHello() {
    fmt.Println("Hello, Goroutine!")
}

func main() {
    go PrintHello()
    fmt.Println("Hello, main!")
    time.Sleep(time.Second)
}

此时,输出结果为:

shell
Hello, main!
Hello, Goroutine!

在上面的代码中,我们在main函数中调用time.Sleep(time.Second)函数,让程序等待1秒钟,以保证Goroutine有足够的时间执行。运行上面的代码,可以看到程序输出了Hello, main!Hello, Goroutine!,说明Goroutine成功执行。

多个Goroutine

Go语言支持创建多个Goroutine,可以轻松实现成千上万个并发Goroutine。下面是一个创建多个Goroutine的示例:

go
package main

import (
    "fmt"
    "time"
)

func PrintHello(i int) {
    fmt.Println("Hello, Goroutine! ", i)
}

func main() {
    for i := 0; i < 10; i++ {
        go PrintHello(i)
    }
    fmt.Println("Hello, main!")
    time.Sleep(time.Second)
}

在上面的代码中,我们创建了10个Goroutine,每个Goroutine的函数体为fmt.Println("Hello, Goroutine! ", i),其中i为Goroutine的编号。运行上面的代码,可以看到程序输出了Hello, main!Hello, Goroutine! 0Hello, Goroutine! 9,但是你会发现,Goroutine的输出顺序是不确定的,这是因为,不同的Goroutine之间的执行顺序是不确定的,所以在实际开发中,不要依赖于Goroutine的执行顺序。

管理Goroutine

,包括runtime.Goexit函数、runtime.Gosched函数、runtime.NumGoroutinesync.WaitGroup函数等。下面是一些常用的管理Goroutine的函数:

runtime.Goexit函数

runtime.Goexit函数可以让当前Goroutine退出,但是不会影响其他Goroutine的执行。下面是一个使用runtime.Goexit函数的示例:

go
package main

import (
	"fmt"
	"runtime"
	"time"
)

func PrintHello() {
	fmt.Println("Hello, Goroutine!")
	runtime.Goexit()
	fmt.Println("Hello, Goroutine! Goexit")
}

func main() {
	go PrintHello()
	fmt.Println("Hello, main!")
	time.Sleep(time.Second)
}

输出结果为:

shell
Hello, main!
Hello, Goroutine!

在上面的代码中,我们在PrintHello函数中调用runtime.Goexit函数,让当前Goroutine退出,但是不会影响其他Goroutine的执行。运行上面的代码,可以看到程序输出了Hello, main!Hello, Goroutine!,但是没有输出Hello, Goroutine! Goexit,说明runtime.Goexit函数成功让当前Goroutine退出,后面的代码不会执行。

runtime.Gosched函数

runtime.Gosched函数可以让当前Goroutine让出CPU,让其他Goroutine执行,但是不会影响当前Goroutine的执行。下面是一个使用runtime.Gosched函数的示例:

go
package main

import (
	"fmt"
	"runtime"
)

func PrintHello() {
	fmt.Println("Hello, Goroutine! ")
}

func main() {
	fmt.Println("Hello, main!")
	go PrintHello()
	runtime.Gosched()
}

输出结果为:

shell
Hello, main!
Hello, Goroutine!

在上面的代码中,我们在main函数中调用runtime.Gosched函数,让当前Goroutine让出CPU,让其他Goroutine执行。运行上面的代码,可以看到程序输出了Hello, main!Hello, Goroutine!,说明runtime.Gosched函数成功让当前Goroutine让出CPU,让其他Goroutine执行,如果没有调用runtime.Gosched函数,程序可能会直接退出,导致Goroutine没有足够的时间执行,从而无法输出Hello, Goroutine!

runtime.NumGoroutine函数

runtime.NumGoroutine函数可以获取当前程序中正在执行的Goroutine的数量。下面是一个使用runtime.NumGoroutine函数的示例:

go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func PrintHello() {
    fmt.Println("Hello, Goroutine!")
}

func main() {
    for i := 0; i < 10; i++ {
        go PrintHello()
    }
    fmt.Println("Hello, main!")
    time.Sleep(time.Second)
    fmt.Println("NumGoroutine: ", runtime.NumGoroutine())
}

输出结果为:

shell
Hello, Goroutine!  3
Hello, main!
NumGoroutine:  10
Hello, Goroutine!  4
Hello, Goroutine!  2
Hello, Goroutine!  5
Hello, Goroutine!  9
Hello, Goroutine!  6
Hello, Goroutine!  7
Hello, Goroutine!  8
Hello, Goroutine!  1
Hello, Goroutine!  0
,包括NumGoroutine的输出结果也是不固定的,因为Goroutine的调度是由Go语言运行时管理的,不同的Goroutine之间的执行顺序是不确定的。

在上面的代码中,我们在main函数中调用runtime.NumGoroutine函数,获取当前程序中正在执行的Goroutine的数量。运行上面的代码,可以看到程序输出了Hello, main!Hello, Goroutine!,并且输出了NumGoroutine: 10,说明当前程序中有10个正在执行的Goroutine。

sync.WaitGroup函数

sync.WaitGroup函数可以等待一组Goroutine执行完毕后再继续执行,可以通过sync.WaitGroup函数实现Goroutine的同步。下面是一个使用sync.WaitGroup函数的示例:

go
package main

import (
	"fmt"
	"sync"
)

func PrintHello(wg *sync.WaitGroup) {
	fmt.Println("Hello, Goroutine!")
	wg.Done()
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go PrintHello(&wg)
	}
	fmt.Println("Hello, main!")
	wg.Wait()
}

在上面的代码中,我们使用sync.WaitGroup函数实现了Goroutine的同步,wg.Add(1)函数用于增加等待的Goroutine数量,wg.Done()函数用于减少等待的Goroutine数量,wg.Wait()函数用于等待所有的Goroutine执行完毕。运行上面的代码,可以看到程序输出了Hello, main!Hello, Goroutine!,并且等待所有的Goroutine执行完毕后才继续执行。

Goroutine的调度

Go语言运行时会自动将Goroutine分配到多个操作系统线程上,实现并发执行。Go语言运行时会根据系统的负载情况自动调整Goroutine的数量,以实现最佳的性能。Go语言运行时会在程序启动时创建一个操作系统线程,称为主线程,主线程会执行main函数,main函数中创建的Goroutine会被分配到主线程上执行。当主线程执行完毕后,Go语言运行时会自动销毁主线程,同时销毁主线程上的所有Goroutine。

Goroutine的通信

通道基础

Goroutine之间可以通过通道(Channel)进行通信,通道是Go语言的一个重要特性,是Goroutine之间的通信机制。通道是一种类型安全的、并发安全的、支持多路复用的通信机制,可以在Goroutine之间传递数据。通道的声明语法如下:

go
var ch chan int

在上面的代码中,我们声明了一个通道ch,通道的元素类型为int。通道的创建语法如下:

go
ch := make(chan int)

在上面的代码中,我们创建了一个通道ch,通道的元素类型为int。通道的发送和接收语法如下:

go
ch <- 1 // 发送数据1到通道ch
x := <-ch // 从通道ch接收数据,并赋值给变量x

在上面的代码中,我们通过ch <- 1向通道ch发送数据1,通过x := <-ch从通道ch接收数据,并赋值给变量x。通道的发送和接收操作都是阻塞的,只有当发送和接收操作都准备就绪时,才会执行发送和接收操作。通道的发送和接收操作可以在不同的Goroutine中执行,通过通道实现Goroutine之间的通信。

单向通道

下面是一个使用通道实现Goroutine之间通信的示例:

go
package main

import (
	"fmt"
)

func PrintHello(ch chan int) {
	fmt.Println("Hello, Goroutine!")
	ch <- 1 // 向通道ch发送数据1
	ch <- 2 // 向通道ch发送数据2
}

func main() {
	ch := make(chan int)
	go PrintHello(ch)
	x1 := <-ch // 从通道ch接收数据
	x2 := <-ch // 从通道ch接收数据
	fmt.Println(x1)
	fmt.Println(x2)
	fmt.Println("Hello, main!")
}

输出结果为:

shell
Hello, Goroutine!
1
2
Hello, main!

在上面的代码中,我们创建了一个通道ch,在PrintHello函数中向通道ch发送数据1,然后在main函数中从通道ch接收数据,并赋值给变量x。运行上面的代码,可以看到程序输出了Hello, Goroutine!1Hello, main!

缓存通道

默认情况下,是无缓冲通道,发送和接收操作是同步的,发送和接收操作必须同时准备就绪才能执行。如果通道是缓存通道,发送和接收操作是异步的,发送和接收操作可以分别准备就绪,然后执行。缓存通道的创建语法如下:

go
ch := make(chan int, 1) // 创建一个缓存通道,缓冲区大小为1

下面是一个使用缓存通道实现Goroutine之间通信的示例:

go
package main

import (
	"fmt"
)

func PrintHello(ch chan int) {
	fmt.Println("Hello, Goroutine!")
	ch <- 1 // 向通道ch发送数据1
	ch <- 2 // 向通道ch发送数据2
}

func main() {
	ch := make(chan int, 1) // 创建一个缓存通道,缓冲区大小为1
	go PrintHello(ch)
	x1 := <-ch // 从通道ch接收数据
	x2 := <-ch // 从通道ch接收数据
	fmt.Println(x1)
	fmt.Println(x2)
	fmt.Println("Hello, main!")
}

输出结果为:

shell
Hello, Goroutine!
1
2
Hello, main!

在上面的代码中,我们创建了一个缓存通道ch,缓存通道的缓冲区大小为1,可以缓存1个元素。在执行过程中,我们向通道ch发送了2个数据,但是通道ch的缓冲区大小为1,所以第二个数据会被缓存起来,直到被接收。运行上面的代码,可以看到程序输出了Hello, Goroutine!12

通道的遍历和关闭

通道可以通过range关键字遍历,通道可以通过close函数关闭。下面是一个使用range关键字遍历通道和使用close函数关闭通道的示例:

go
package main

import (
    "fmt"
)

func PrintHello(ch chan int) {
    fmt.Println("Hello, Goroutine!")
    ch <- 1 // 向通道ch发送数据1
    ch <- 2 // 向通道ch发送数据2
    close(ch) // 关闭通道ch
}

func main() {
    ch := make(chan int)
    go PrintHello(ch)
    for x := range ch { // 遍历通道ch
        fmt.Println(x)
    }
    fmt.Println("Hello, main!")
}

输出结果为:

shell
Hello, Goroutine!
1
2
Hello, main!

在上面的代码中,我们使用range关键字遍历通道ch,通道chPrintHello函数中被关闭,遍历通道ch时,会自动判断通道ch是否已经关闭,如果通道ch已经关闭,遍历会自动结束。运行上面的代码,可以看到程序输出了Hello, Goroutine!12Hello, main!

Select

select语句可以实现,可以同时等待多个通道的操作,一旦某个通道准备就绪,就会执行对应的操作。select语句的语法如下:

go
select {
case x := <-ch1:
    // 从通道ch1接收数据,并赋值给变量x
case ch2 <- 1:
    // 向通道ch2发送数据1
default:
    // 默认操作
}

下面是一个使用select语句实现多路复用的示例:

go
package main

import (
    "fmt"
)

func PrintHello(ch1, ch2 chan int) {
    fmt.Println("Hello, Goroutine!")
    ch1 <- 1 // 向通道ch1发送数据1
    ch2 <- 2 // 向通道ch2发送数据2
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go PrintHello(ch1, ch2)
    select {
    case x1 := <-ch1:
        fmt.Println(x1)
    case x2 := <-ch2:
        fmt.Println(x2)
    }
    fmt.Println("Hello, main!")
}

输出结果为:

shell
Hello, Goroutine!
1
Hello, main!

在上面的代码中,我们使用select语句实现了多路复用,select语句同时等待通道ch1和通道ch2的操作,一旦通道ch1或通道ch2准备就绪,就会执行对应的操作。运行上面的代码,可以看到程序输出了Hello, Goroutine!1Hello, main!。 在实际开发中,Select语句是非常常用的,比如在网络编程中,可以使用Select语句同时等待多个网络连接的操作。

总结

Goroutine作为Go语言的重要组成部分,在此篇学习笔记中,我们详细介绍了Goroutine的创建、管理、调度、通信等方面的知识,包括channel通信,在实际开发中,需要熟练掌握Goroutine的使用,才能更好地实现Go并发编程。

问卷调查

在本篇学习笔记中,在多处使用了AI快捷搜索对话功能,作为读者,你认为这种功能对你的文章阅读有帮助吗?您可点击以下链接参与问卷调查,帮助我们更好地改进产品,本次问卷为有奖问卷,包括PostChat会员和现金奖励,感谢您的参与!点击参与问卷调查

评论 隐私政策