Goroutine与Channel

协程(Co-routine)与线程(Thread)

内存空间被划分为内核空间与用户空间,其中内核空间仅对CPU可见。

Go语言调度器(GMP):

​ G——>goroutine协程

​ M——>processor处理器

​ P——>thread线程

image-20250219161841931

调度器特点:

  • 复用线程:

    • work stealing机制:若P的本地队列为空,可获取其他队列不为空的其他P队列中的协程。
    • hand off机制:若当前P阻塞,则创建或唤醒新P,让新P接管被阻塞的队列。
  • 利用并行:可设置GOMAXPROCS限定P的个数

  • 抢占:可剥夺当前执行进程。
  • 全局G队列:在执行work stealing策略时,该队列在解锁或加锁后,可供P使用。

Goroutine的创建与退出

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func newTask(){
i:=0
for{
if(i<10){
i++
fmt.Printf("goroutine:%d\n",i)
time.Sleep(1*time.Second)
}
}
}

//使用 go关键字 + [函数]:即创建goroutine
func main() {
//创建一个goroutine
go newTask()
i:=0
for{
if(i<10){
i++
fmt.Printf("Mainroutine:%d\n",i)
time.Sleep(1*time.Second)
}
}
fmt.Println("End")
}

注意,goroutine的生命周期受到父线程控制,如上述例子中,若main线程结束,则newTask也将被结束。

执行runtime.Goexit()可结束当前goroutine。

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
//创建一个goroutine
go func(){
defer fmt.Println("a.defer")
func(){
defer fmt.Println("b.defer")
//退出当前的goroutine
runtime.Goexit()
}()
}()

for{
time.Sleep(1*time.Second)
}
}

匿名函数的调用

go
1
2
3
4
5
6
7
func main(){
//在匿名函数后跟中括号,即传入对应参数并调用。
func(a int,b int){
fmt.Printf("this is %d\n",a)
fmt.Printf("this is %d\n",b)
}(10,12)
}

管道(Channel)的定义与使用

channel内部实现goroutine间的同步。

go
1
2
3
4
5
6
7
8
9
10
11
12
func main() {
//创建一个通道channel
c:=make(chan int)

go func(){
//将666传入channel中
c <- 666
}()
//读出channel中的值并写入num
num:=<-c
fmt.Println(num)
}

channel分为有缓存channel与无缓冲channel,

  • 有缓存channel:输入方可随时退出channel,直到channel为满;接受方可随时取出数据,直到channel为空。

    go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func main() {
    //创建一个缓存区为3的channel
    c:=make(chan int,3)
    go func(){
    defer fmt.Println("goroutine end")
    for i:=0;i<4;i++{
    c<-i
    //使用len(c),cap(c)可以查看当前channel的长度与容量。
    fmt.Println(i,"len(c)=",len(c),"cap(c)=",cap(c))
    }
    }()

    time.Sleep(2*time.Second)
    //依次从channel中读出数据
    for i:=0;i<4;i++{
    num:=<-c
    fmt.Println(num)
    }
    fmt.Println("main end")
    }
  • 无缓冲channel:输入方将数据传入channel后,直到接收方进入channel取出数据,才能退出channel。

管道的关闭

使用close(channel)可以关闭该通道。

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
c:=make(chan int,3)
go func(){
for i:=0;i<4;i++{
c<-i
}
//关闭通道c
close(c)
}()
for {
if data,ok:=<-c;ok{
fmt.Println(data)
}else{
break
}
}
fmt.Println("Main finished...")
}

注意:

  • 关闭channel后,若再向channel发送数据,则会导致panic错误并返回零值。
  • 关闭channel后,仍可以从channel中读出数据。
  • 对于nil channel(未分配空间的channel),无论收发都会被阻塞。

关键字Range与Select

对于如下代码段,可以使用 range 对代码进行优化:

go
1
2
3
4
5
6
7
for {
if data,ok:=<-c;ok{
fmt.Println(data)
}else{
break
}
}

简化代码如下:

go
1
2
3
4
//迭代地从channel中读出数据
for data := range c{
fmt.Println(data)
}

使用 Select 能够监控多个channel的状态:

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//使用select监听多个channel
select{

//若channel可读,则执行case1
case <- chan1:
option1
...
//若channel可写,则执行case2
case chan2 <- 711
option2
...
//否则,执行default
default
...
}