1. goroutine的基本概念介绍
1.1 引入
进程与线程的说明:进程与线程
举例(这是我见过最好理解的例子了):百度网盘的启动之后,百度网盘软件就是一个进程。百度网盘下载文件时,可以同时下载好几个文件,此时每一个文件的下载过程就是线程。
并发与并行的区别:
1)多线程程序在单核上运行,就是并发。(同一时刻,只有一个任务在执行)
2)多线程程序在多核上运行,就是并行。
1.2 Go协程和Go主线程
协程是轻量级的线程。goroutinue可以实现并行,也就是说,多个协程可以在多个处理器同时跑。而协程同一时刻只能在一个处理器上跑(把宿主语言想象成单线程的就好了)。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。
Go协程的特点:
1)有独立的栈空间
2)共享程序堆空间
3)调度由用户控制
4)协程是轻量级的线程
1.3 案例学习
针对以上程序,在运行过程中的流程如下图所示:
1.4 goroutine的调度模型
Go的调度器内部有四个重要的结构:M,P,S,Sched,如上图所示(Sched未给出)
- M:M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息
- G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
- P:P全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine
- Sched:代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。
1.5 golang设置CPU运行数目
2. channel的使用
channel是goroutinues之间进行通信的利器。channel可以形象比喻为工厂里的传送带,一头的生产者goroutine往传输带放东西,另一头的消费者goroutinue则从输送带取东西。channel实际上是一个有类型的消息队列,遵循先进先出的特点。
2.1 channel的基本介绍
1)channel类似于一个队列。
2)channel中的数据是先进先出FIFO。
3)线程安全。多goroutine访问时,不需要加锁操作。
4)channel是有数据类型的。一个string的channel 只能存放string 类型数据。
2.2 channel的初步使用
- 定义与声明
var 变量名 chan 数据类型
channel变量必须初始化后才可以使用,即就是make。
- 初始化
var intChan chan int
intChan = make( chan int,3 )
- 操作符号
intChan <- ele 表示ele被发送给channel intChan ;
ele2 <- intChan 表示从channel intChan 取一个值,然后赋给ele2
- 注意事项
1)channel中只能存放指定类型的数据
2)channel的数据放满之后,就不能再放了,在初始化的时候确定了channel的容量
3)在没有使用协程的情况下,如果channel数据取完了,再取数据,程序会报错。
- 管道的关闭和遍历
-
内建函数 close(intChan) //这时不能在往channel里面写入数据
遍历使用for-range方式:遍历时未关闭管道,出现deadlock错误;遍历时关闭管道,正常遍历结束。
2.3 channel的阻塞机制
writeData协程 :往管道中写入数据,总共要写50个,管道的容量是10;
readData协程:关闭读协程;
此时,管道出现阻塞,程序报deadlock错误。因为管道已满,没有被读取,无法再继续写入数据。
3 goroutine和channel的应用实例
3.1 要求统计1-80000的数字中,哪些是素数?
效果演示
package main
import(
"fmt"
)
var (
numChan = make(chan int, 2000)
resChan = make(chan map[int]int,2000)
exitChan = make(chan bool, 8)
)
func write(){
for i:=1;i<=2000;i++{
numChan<- i
}
close(numChan)
}
func calc(){
for{
v,ok := <- numChan
if !ok{
break
}
num := 0
for i:=1;i<=v;i++{
num += i
}
res := make(map[int]int)
res[v] = num
resChan<- res
}
fmt.Println("一个协程取不到数据")
exitChan<- true
}
func main(){
go write()
for i:=0;i<8;i++{
go calc()
}
go func(){
for i:=0;i<8;i++{
<- exitChan
}
close(resChan)
close(exitChan)
}()
for{
res , ok := <-resChan
if !ok{
break
}
for index, sum := range res{
fmt.Printf("得到的计算结果是res[%d]=%d\n",index,sum)
}
}
}
3.2 生产与消费者应用:
https://blog.csdn.net/littleschemer/article/details/70232659
4. 补充
1)channel可以定义为只读,或者只写的。默认情况是可读可写的。
2)使用select可以解决从管道取数据的阻塞问题。
3)goroutine中使用recover,解决协程中出现panic。