Golang并发模型:Goroutine 与 Channel 初探
文章目录
- goroutine
- goexit()
- channel
- 缓冲
- close
- range
- select
goroutine
goroutine
是 Go 语言中的一种轻量级线程(lightweight thread),由 Go 运行时环境管理。与传统的线程相比,goroutine 的创建和销毁的开销很小,可以轻松创建成千上万个 goroutine,而不会导致系统性能下降。
以下是一些关于 goroutine 的重要特性:
-
轻量级: 每个 goroutine 的栈大小初始时只有几 KB,可以根据需要进行动态扩展和收缩。
-
并发执行: Go 语言通过 goroutine 实现并发编程,允许在程序中同时执行多个任务。通过 goroutine,可以更方便地编写并发程序。
-
独立调度: Go 运行时具有自己的调度器,它负责在多个 goroutine 之间进行协作式调度。与操作系统线程不同,goroutine 的调度是在用户空间进行的,减少了上下文切换的成本。
-
通信通过通道: 多个 goroutine 之间通过通道进行通信。通道是一种数据结构,用于在 goroutine 之间传递数据。通过使用通道,可以更安全、更简单地实现 goroutine 之间的通信和同步。
以下是一个简单的示例,演示了如何创建和运行一个 goroutine:
package main
import (
"fmt"
"time"
)
// 子goroutine
func newTask() {
for i := 0; ; i++ {
fmt.Printf("new goroutine:i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
// 主goroutine
func main() {
// 创建一个go程 去执行newTask()流程
go newTask()
for i := 0; ; i++ {
fmt.Printf("main goroutine:i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
由运行结果可以看出,newTask
函数被启动为一个 goroutine,与主 goroutine 同时执行。由于 goroutine 是并发执行的,输出将交替显示两个 goroutine 的执行结果。
但是,当主goroutine执行任务完成退出比子goroutine早时,程序就会直接终止,而不再等待其他的 goroutine 执行完毕。如以下代码所示。
package main
import (
"fmt"
"time"
)
// 子goroutine
func newTask() {
for i := 0; ; i++ {
fmt.Printf("new goroutine:i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
// 主goroutine
func main() {
// 创建一个go程 去执行newTask()流程
go newTask()
fmt.Println("main goroutine exit")
}
/*输出结果
main goroutine exit
*/
goexit()
runtime.Goexit()
是 Go 语言 runtime 包提供的一个函数,用于立即终止当前 goroutine 的执行。调用 Goexit 会导致当前 goroutine 进行善后工作(执行已注册的 defer 语句)并返回,而不会影响其他正在运行的 goroutine。
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 用go创建一个形参为空,返回值为空的函数
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit()
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
time.Sleep(1 * time.Second)
}
}
/*输出结果
B.defer
A.defer
*/
在 goroutine 内部的第二个 defer 语句 defer fmt.Println("B.defer")
表示在该 goroutine 返回之前执行。然后,在这个 goroutine 内部的匿名函数中调用了 runtime.goexit()
,这会立即结束当前 goroutine 的执行。
由于 runtime.goexit()
的调用,后面的代码不再执行,包括 “B” 的打印语句和 “A” 的打印语句。
channel
在Go语言中,channel
是一种用于在 goroutine 之间进行通信和同步的数据结构。它提供了一种安全、简单且高效的方式,使得不同的 goroutine 之间能够传递数据和同步操作。
channel的定义:c := make(chan int)
。
下面是进行 goroutine 间通信的基本例子。因为 channel 是阻塞的,所以在主 goroutine 中接收数据的操作 num := <-c
会等待直到 goroutine 发送完数据为止。
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
defer fmt.Println("goroutine end")
fmt.Println("goroutine正在运行")
c <- 666 //将666发送给c
}()
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num =", num)
fmt.Println("main goroutine end")
}
/*输出结果
goroutine正在运行
goroutine end
num = 666
main goroutine end
*/
缓冲
上面例子中的channel是一个无缓冲的,其运行的具体过程如下图所示。
在给channel加上缓冲后,运行过程如下图。
下面这段代码演示了使用带有缓冲的 channel 进行 goroutine 间通信的例子。由于通道带有缓冲,即使没有立即被接收,子 goroutine 依然可以向通道发送多个数据,直到达到缓冲容量。在主 goroutine 中,使用缓冲通道可以使得发送和接收操作更灵活,不需要等待另一方立即接收。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) // 带有缓冲的channel
fmt.Println("len(c) =", len(c), "cap(c) =", cap(c))
go func() {
defer fmt.Println("子go程结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Println("子go程正在运行,len(c) =", len(c), "cap(c) =", cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <-c
fmt.Println("num =", num)
}
fmt.Println("主go程结束")
}
/*运行结果
len(c) = 0 cap(c) = 3
子go程正在运行,len(c) = 1 cap(c) = 3
子go程正在运行,len(c) = 2 cap(c) = 3
子go程正在运行,len(c) = 3 cap(c) = 3
子go程正在运行,len(c) = 3 cap(c) = 3
子go程结束
num = 0
num = 1
num = 2
num = 3
主go程结束
*/
close
close
函数用于关闭一个通道。
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 3; i++ {
c <- i
}
close(c) // close可以关闭一个channel
}()
for {
// ok如果为true表示channel没有关闭,如果为false表示已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main goroutine end")
}
channel无需像文件一样经常关闭,只有确实不需要发数据了,或者想显式结束range循环,才去关闭channel。
关闭channel后,无法再向channel发送数据,但是可以继续接收数据。
对于nil channel,无论收发都会被阻塞。
range
当使用 range 迭代通道时,它会一直阻塞,直到通道关闭且所有的元素都被读取完毕。需要注意的是,在使用 range 迭代通道时,通道必须在某个地方被关闭,否则 range 不会结束。否则,它会一直等待新的数据,导致程序阻塞。
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 3; i++ {
c <- i
}
close(c) // close可以关闭一个channel
}()
// 可以使用range来迭代不断操作channel
for data := range c {
fmt.Println(data)
}
fmt.Println("main goroutine end")
}
select
单流程下一个go只能监控一个channel的状态,select可以完成监控多个channel
基本语法:
select {
case <- chan1:
// 如果chan1成功读取到数据,就会进入该case并处理
case chan2 <- 1:
// 如果成功向chan2写入数据,就会进入该case并处理
default:
// 如果上面都没有成功,就进入default处理
}
下面是一个通过 Go 语言的 select 语句和通道(channel)来生成斐波那契数列的例子。
package main
import "fmt"
func fibonacii(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
// 如果c可写,那么就会进入该case
x = y
y = x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacii(c, quit)
}
这个程序的输出是斐波那契数列的前 10 个数字,然后输出 “quit”。由于 main 函数中的 goroutine 先执行,因此在 quit 通道被关闭之前,fibonacci 函数中的循环会一直运行。