go协程的初步认识

协程到底是什么东西?

go的协程是为了解决多核CPU利用率问题,go语言层面并不支持多进程或多线程,但是协程更好用,协程被称为用户态线程,不存在CPU上下文切换问题,效率非常高。

实例

1.Hello World

package main

func main() {
    go say("Hello World")
}

func say(s string) {
    println(s)
}
复制代码

go启动协程的方式就是使用关键字go,后面一般接一个函数或者匿名函

这至少说明你代码写的没问题,当你使用go启动协程之后,后面没有代码了,这时候主线程结束了,这个协程还没来得及执行就结束了... 聪明的小伙伴会想到,那我主线程先睡眠1s等一等? Yes, 在main代码块最后一行加入:

time.Sleep(time.Second*1) # 表示睡眠1s
复制代码

再次运行,打印出Hello World,1s后程序终止!

2.WaitGroup

上面睡眠这种做法肯定是不靠谱的,WaitGroup可以解决这个问题, 代码如下:

package main

import (
	"sync"
)
var wg = sync.WaitGroup{}

func main() {
	wg.Add(1)
	go say("Hello World")
	wg.Wait()
}

func say(s string) {
	println(s)
	wg.Done()
}
复制代码

简单说明一下用法,var 是声明了一个全局变量 wg,类型是sync.WaitGroup,wg.add(1) 是说我有1个协程需要执行,wg.Done 相当于 wg.Add(-1) 意思就是我这个协程执行完了。wg.Wait() 就是告诉主线程要等一下,等协程都执行完再退出。

3.并发还并行?

当你同时启动多个协程的时候,会怎么执行呢?

package main

import (
	"strconv"
	"sync"
)

var wg = sync.WaitGroup{}

func main() {
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go say("Hello World: " + strconv.Itoa(i))
	}
	wg.Wait()
}

func say(s string) {
	println(s)
	wg.Done()
}
复制代码

如果去掉go,直接在循环里面调用这个函数5次,毫无疑问会一次输出 Hello World: 0 ~ 4, 但是在协程里面,输出的顺序是无序的,看上去像是“同时执行”,其实这只是并发。

有一个问题,上面的例子里面是并发还并行呢?

首先,我们得区分什么是并发什么是并行,举个比较熟悉的例子,并发就是一个锅同时炒2个菜,2个菜来回切换,并行就是你有多个锅,每个锅炒不同的菜,多个锅同时炒!

回到上面的例子,如果当前CPU是单核,那么上面程序就是并发执行,如果当前CPU是多核,那就是并行执行,结果都是一样的,如何证明请看下面的例子:

package main

import (
	"runtime"
	"strconv"
)

func main() {
	runtime.GOMAXPROCS(1)
	for i := 0; i < 5; i++ {
		go say("Hello World: " + strconv.Itoa(i))
	}
	for {
	}
}

func say(s string) {
	println(s)
}
复制代码

默认情况下,最新的go版本协程可以利用多核CPU,但是通过runtime.GOMAXPROCS() 我们可以设置所需的核心数(其实并不是CPU核心数),在上面的例子我们设置为1,也就是模拟单核CPU,运行这段程序你会发现无任何输出,如果你改成2,你会发现可以正常输出。

这段程序逻辑很简单,使用一个for循环启动5个协程,然后写了一个for死循环,如果是单核CPU,当运行到for死循环的时候,由于没有任何io操作(或者能让出CPU的操作),会一直卡在那里,但是如果是多核CPU,go协程就会调用其它CPU去执行。

如果你在for循环里面加入一个sleep操作,比如下面这样:

for {
		time.Sleep(time.Second)
	}
复制代码

运行上面代码,你会发现居然可以正常输出,多次运行你会发现其结果一直是从0到4,变成顺序执行了!这也说明单核CPU只能并发,不能并行!

4.channel

channel,又叫管道,在go里面的管道是协程之间通信的渠道,类似于咱们常用的消息队列。在上面的例子里面我们是直接打印出来结果,假如现在的需求是把输出结果返回到主线程呢?

package main

import (
	"strconv"
)

func main() {
	var result = make(chan string)
	for i := 0; i < 5; i++ {
		go say("Hello World: "+strconv.Itoa(i), result)
	}
	for s := range result {
		println(s)
	}
}

func say(s string, c chan string) {
	c <- s
}
复制代码

简单说明一下,这里就是实例化了一个string类型的管道,在调用函数的时候会把管道当作参数传递过去,然后在调用函数里面我们不输出结果,然后把结果通过管道返还回去,然后再主线程里面我们通过for range循环依次取出结果!

结果如下,但是这个程序是有bug的,在程序的运行的最后会出现一个fatal error,提示所有的协程都进入睡眠状态,死锁!

Hello World: 4
Hello World: 1
Hello World: 0
Hello World: 2
Hello World: 3
fatal error: all goroutines are asleep - deadlock!
复制代码

go的管道默认是阻塞的(假如你不设置缓存的话),你那边放一个,我这头才能取一个,如果你那边放了东西这边没人取,程序就会一直等下去,死锁了,同时,如果那边没人放东西,你这边取也取不到,也会发生死锁!

如何解决这个问题呢?标准的做法是主动关闭管道,或者你知道你应该什么时候关闭管道, 当然你结束程序管道自然也会关掉!针对上面的演示代码,可以这样写:

var i = 0
for s := range result {
    println(s)
    if i >= 4 {
        close(result)
    }
    i++
}
复制代码

Guess you like

Origin juejin.im/post/7047452785918345253