初识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 func() {
fmt.Println("Hello, Goroutine!")
// go func()函数体
}()基础示例
基本用法:
// Goroutine 外部函数
package main
import (
"fmt"
"time"
)
func PrintHello() {
fmt.Println("Hello, Goroutine!")
}
func main() {
go PrintHello()
time.Sleep(time.Second)
}// 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的执行效果:
package main
import (
"fmt"
"time"
)
func PrintHello() {
fmt.Println("Hello, Goroutine!")
}
func main() {
go PrintHello()
fmt.Println("Hello, main!")
}此时,输出结果为:
Hello, main!如果你是初学者,你肯定好奇为什么没有输出Hello, Goroutine!,这是因为PrintHello函数相当于异步执行了,main函数执行完毕后,程序就退出了,Goroutine没有足够的时间执行。为了让Goroutine有足够的时间执行,我们可以在main函数中调用time.Sleep(time.Second)函数,让程序等待1秒钟,以保证Goroutine有足够的时间执行。
package main
import (
"fmt"
"time"
)
func PrintHello() {
fmt.Println("Hello, Goroutine!")
}
func main() {
go PrintHello()
fmt.Println("Hello, main!")
time.Sleep(time.Second)
}此时,输出结果为:
Hello, main!
Hello, Goroutine!在上面的代码中,我们在main函数中调用time.Sleep(time.Second)函数,让程序等待1秒钟,以保证Goroutine有足够的时间执行。运行上面的代码,可以看到程序输出了Hello, main!和Hello, Goroutine!,说明Goroutine成功执行。
多个Goroutine
Go语言支持创建多个Goroutine,可以轻松实现成千上万个并发Goroutine。下面是一个创建多个Goroutine的示例:
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! 0到Hello, Goroutine! 9,但是你会发现,Goroutine的输出顺序是不确定的,这是因为,不同的Goroutine之间的执行顺序是不确定的,所以在实际开发中,不要依赖于Goroutine的执行顺序。
管理Goroutine
,包括runtime.Goexit函数、runtime.Gosched函数、runtime.NumGoroutine、sync.WaitGroup函数等。下面是一些常用的管理Goroutine的函数:runtime.Goexit函数
runtime.Goexit函数可以让当前Goroutine退出,但是不会影响其他Goroutine的执行。下面是一个使用runtime.Goexit函数的示例:
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)
}输出结果为:
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函数的示例:
package main
import (
"fmt"
"runtime"
)
func PrintHello() {
fmt.Println("Hello, Goroutine! ")
}
func main() {
fmt.Println("Hello, main!")
go PrintHello()
runtime.Gosched()
}输出结果为:
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函数的示例:
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())
}输出结果为:
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! 0NumGoroutine的输出结果也是不固定的,因为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函数的示例:
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之间传递数据。通道的声明语法如下:
var ch chan int在上面的代码中,我们声明了一个通道ch,通道的元素类型为int。通道的创建语法如下:
ch := make(chan int)在上面的代码中,我们创建了一个通道ch,通道的元素类型为int。通道的发送和接收语法如下:
ch <- 1 // 发送数据1到通道ch
x := <-ch // 从通道ch接收数据,并赋值给变量x在上面的代码中,我们通过ch <- 1向通道ch发送数据1,通过x := <-ch从通道ch接收数据,并赋值给变量x。通道的发送和接收操作都是阻塞的,只有当发送和接收操作都准备就绪时,才会执行发送和接收操作。通道的发送和接收操作可以在不同的Goroutine中执行,通过通道实现Goroutine之间的通信。
单向通道
下面是一个使用通道实现Goroutine之间通信的示例:
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!")
}输出结果为:
Hello, Goroutine!
1
2
Hello, main!在上面的代码中,我们创建了一个通道ch,在PrintHello函数中向通道ch发送数据1,然后在main函数中从通道ch接收数据,并赋值给变量x。运行上面的代码,可以看到程序输出了Hello, Goroutine!、1和Hello, main!。
缓存通道
默认情况下,是无缓冲通道,发送和接收操作是同步的,发送和接收操作必须同时准备就绪才能执行。如果通道是缓存通道,发送和接收操作是异步的,发送和接收操作可以分别准备就绪,然后执行。缓存通道的创建语法如下:
ch := make(chan int, 1) // 创建一个缓存通道,缓冲区大小为1下面是一个使用缓存通道实现Goroutine之间通信的示例:
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!")
}输出结果为:
Hello, Goroutine!
1
2
Hello, main!在上面的代码中,我们创建了一个缓存通道ch,缓存通道的缓冲区大小为1,可以缓存1个元素。在执行过程中,我们向通道ch发送了2个数据,但是通道ch的缓冲区大小为1,所以第二个数据会被缓存起来,直到被接收。运行上面的代码,可以看到程序输出了Hello, Goroutine!、1和2。
通道的遍历和关闭
通道可以通过range关键字遍历,通道可以通过close函数关闭。下面是一个使用range关键字遍历通道和使用close函数关闭通道的示例:
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!")
}输出结果为:
Hello, Goroutine!
1
2
Hello, main!在上面的代码中,我们使用range关键字遍历通道ch,通道ch在PrintHello函数中被关闭,遍历通道ch时,会自动判断通道ch是否已经关闭,如果通道ch已经关闭,遍历会自动结束。运行上面的代码,可以看到程序输出了Hello, Goroutine!、1、2和Hello, main!。
Select
select语句可以实现,可以同时等待多个通道的操作,一旦某个通道准备就绪,就会执行对应的操作。select语句的语法如下:
select {
case x := <-ch1:
// 从通道ch1接收数据,并赋值给变量x
case ch2 <- 1:
// 向通道ch2发送数据1
default:
// 默认操作
}下面是一个使用select语句实现多路复用的示例:
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!")
}输出结果为:
Hello, Goroutine!
1
Hello, main!在上面的代码中,我们使用select语句实现了多路复用,select语句同时等待通道ch1和通道ch2的操作,一旦通道ch1或通道ch2准备就绪,就会执行对应的操作。运行上面的代码,可以看到程序输出了Hello, Goroutine!、1和Hello, main!。 在实际开发中,Select语句是非常常用的,比如在网络编程中,可以使用Select语句同时等待多个网络连接的操作。
总结
Goroutine作为Go语言的重要组成部分,在此篇学习笔记中,我们详细介绍了Goroutine的创建、管理、调度、通信等方面的知识,包括channel通信,在实际开发中,需要熟练掌握Goroutine的使用,才能更好地实现Go并发编程。
问卷调查
在本篇学习笔记中,在多处使用了AI快捷搜索对话功能,作为读者,你认为这种功能对你的文章阅读有帮助吗?您可点击以下链接参与问卷调查,帮助我们更好地改进产品,本次问卷为有奖问卷,包括PostChat会员和现金奖励,感谢您的参与!点击参与问卷调查