Go第 16 章 :goroutine 和 channel
16.1 goroutine-看一个需求
需求:要求统计 1-9000000000 的数字中,哪些是素数?
分析思路:
- 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
- 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到 goroutine.【速度提高 4 倍】
16.2 goroutine-基本介绍
16.2.1 进程和线程介绍
16.2.2 程序、进程和线程的关系示意图
16.2.3 并发和并行
并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
- 示意图:
16.2.4 Go 协程和 Go 主线程
Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以
这样理解,协程是轻量级的线程[编译器做优化]。
Go 协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由程序员控制
- 协程是轻量级的线程
16.3 goroutine-快速入门
16.3.1 案例说明
请编写一个程序,完成如下功能:
- 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 “hello,world”
- 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
- 要求主线程和 goroutine 同时执行.
- 画出主线程和协程执行流程图
16.3.2 快速入门小结
- 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
- Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一
般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了
16.4 goroutine 的调度模型
16.4.1 MPG 模式基本介绍
16.4.2 MPG 模式运行的状态 1
16.4.3 MPG 模式运行的状态 2
16.5 设置 Golang 运行的 cpu 数
介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目
16.6 channel(管道)-看个需求
需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map中。最后显示出来。 要求使用 goroutine完成
package main
import (
"fmt"
"time"
)
var (
myMap =make(map[int]int,10)
)
// test 函数就是计算 n!, 让将这个结果放入到 myMa
func test(n int){
res:=1
for i:=1;i<=n;i++{
res *=i
}
//这里我们将 res 放入到 myMap
myMap[n]=res //concurrent map writes?
}
func main(){
// 我们这里开启多个协程完成这个任务[200 个]
for i:=1;i<=200;i++{
go test(i)
}
//休眠 10 秒钟【第二个问题 】
time.Sleep(time.Second*10)
//这里我们输出结果,变量这个结果
for i,v:=range myMap{
fmt.Printf("map[%d]=%d\n",i,v)
}
}
16.6.1 不同 goroutine 之间如何通讯
-
- 全局变量的互斥锁
-
- 使用管道 channel 来解决
16.6.2 使用全局变量加锁同步改进程
因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent mapwrites
解决方案:加入互斥锁
我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)
代码改进
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap =make(map[int]int,10)
//声明一个全局的互斥锁
//lock:是一个全局的互斥锁
//sync是包:synchornized同步
//Mutex:是互斥
lock sync.Mutex
)
// test 函数就是计算 n!, 让将这个结果放入到 myMa
func test(n int){
res:=1
for i:=1;i<=n;i++{
res *=i
}
//这里我们将 res 放入到 myMap
//加锁
lock.Lock()
myMap[n]=res //concurrent map writes?
lock.Unlock()
}
func main(){
// 我们这里开启多个协程完成这个任务[200 个]
for i:=1;i<=200;i++{
go test(i)
}
//休眠 10 秒钟【第二个问题 】
time.Sleep(time.Second*10)
//这里我们输出结果,变量这个结果
lock.Lock()
for i,v:=range myMap{
fmt.Printf("map[%d]=%d\n",i,v)
}
lock.Unlock()
}
16.6.3 为什么需要 channel
16.6.4 channel 的基本介绍
解释:main遍历map的时候有可能协程没执行完,那遍历map的时候有可能和协程同时操作map所以韩老师在这里也加锁
- channle 本质就是一个数据结构-队列【示意图】
- 数据是先进先出【FIFO : first in first out】
- 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
- channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
- 示意图:
16.6.5 定义/声明 channel
- var 变量名 chan 数据类型
- 举例:
var intChan chan int (intChan 用于存放 int 数据)
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
var perChan chan Person
var perChan2 chan *Person
…
-说明
channel 是引用类型
channel 必须初始化才能写入数据, 即 make 后才能使用
管道是有类型的,intChan 只能写入 整数 int
16.6.6 管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
package main
import "fmt"
func main(){
//演示一下管道的使用
//1、创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int,3)
//2、看看intChan是什么
fmt.Printf("intChan的值=%v intChan 本身的地址 =%p\n",intChan,&intChan)
//3、向g管道写入数据
intChan <-10
num:=21
intChan<-num
intChan<-50
// intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量
//4. 看看管道的长度和 cap(容量)
fmt.Printf("channel len =%v cap=%v\n",len(intChan),cap(intChan))
//5. 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=",num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3
//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
num3 := <-intChan
num4 := <-intChan
//num5 := <-intChan
//fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
fmt.Println("num3=", num3, "num4=", num4)
}
16.6.7 channel 使用的注意事项
- channel 中只能存放指定的数据类型
- channle 的数据放满后,就不能再放入了
- 如果从 channel 取出数据后,可以继续放入
- 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock
16.6.8 读写 channel 案例演示
有的情况需要使用类型断言
16.7 管道的课后练习题
package main
import (
"fmt"
"math/rand"
"strconv"
)
type Person struct{
Name string
Age int
Address string
}
func WriteChan(PersonChan chan Person){
for i:=0;i<10;i++{
PersonChan <-Person{
Name: "Person"+strconv.Itoa(rand.Intn(100)),
Age:rand.Intn(100),
Address:"成都市成华大道"+strconv.Itoa(rand.Intn(100))+"号",
}
fmt.Println("write data=",i)
}
close(PersonChan)
}
func ReadChan(PersonChan chan Person,exitChan chan bool){
i:=0
for{
i++
v,ok:=<-PersonChan
if !ok{
break
}
fmt.Printf("第%v个perosn数据的Name=%v,Age=%v,Address=%v\n",i,v.Name,v.Age,v.Address)
}
exitChan <-true
close(exitChan)
}
func main(){
PersonChan:=make(chan Person,100)
exitChan:=make(chan bool,1)
go WriteChan(PersonChan)
go ReadChan(PersonChan,exitChan)
for {
_, ok := <- exitChan
if !ok {
break
}
}
}
16.8 channel 的遍历和关闭
16.8.1 channel 的关闭
使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然 可以从该 channel 读取数据
案例演示:
16.8.2 channel 的遍历
channel 支持 for–range 的方式进行遍历,请注意两个细节 1) 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误 2) 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
16.8.3 channel 遍历和关闭的案例演示
看代码演示:
16.8.4 应用实例 1
package main import (
"fmt"
_ "time" )
//write Data
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
//放入数据
intChan<- i
fmt.Println("writeData ", i)
//time.Sleep(time.Second)
}
close(intChan) //关闭
}
//read data
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
//time.Sleep(time.Second)
fmt.Printf("readData 读到数据=%v\n", v)
}
//readData 读取完数据后,即任务完成
exitChan<- true close(exitChan)
}
func main() {
//创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
//time.Sleep(time.Second * 10)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
16.8.5 应用实例 2-阻塞
16.8.6 应用实例 3
需求:
要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine 和 channel 的知识后,就可以完成了 [测试数据: 80000]
分析思路:
传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。
使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine去完成,完成任务时间短。
前一节老师说了,写满的时候这个协程会阻塞的,但是不会报错,因为编译器知道有协程在读,等从管道中取出数字之后就可以继续写了 intchan :=make(chan int 1000)
package main
import (
"fmt"
"time"
)
func putNum(intChan chan int) {
for i := 0; i < 80000; i++ {
intChan <- i
}
close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
time.Sleep(time.Millisecond * 10)
num, ok := <-intChan
if !ok {
break
}
flag = true
for i := 2; i < (num+1)/2; i++ {
if num%i == 0 { //说明该 num 不是素数
flag = false
break
}
}
if flag {
primeChan <- num
}
}
fmt.Println("有一个primeChan协程因为取不到数据,退出")
//这里我们还不能关闭 primeChan,因为有可能别人还在处理
//向 exitChan 写入 true
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 20000)
//标识退出的管道
exitChan := make(chan bool, 1024) //4个
//开始统计时间
start:=time.Now().Unix()
//开启一个协程,向intChan放入1-80000个数
go putNum(intChan)
//开启4个协程,从intChan取出数据,并判断是否为素数,如果是,就放入到primeChan
for i := 0; i < 1024; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//这里我们主线程,进行处理
//直接
go func() {
for i := 0; i < 1024; i++ {
<-exitChan//取不到4个就等待,取到了就扔出去
}
//结束时间统计
end:=time.Now().Unix()
fmt.Printf("使用协程耗时=%ds\n",end-start)
//当我们从exitChan取出来4个结果,就可以放心的关闭primeChan
close(primeChan)
}()
//遍历我们的primeChan把结果取出
for {
_, ok := <-primeChan
if !ok {
break
}
//fmt.Printf("素数=%d\n", res)
}
fmt.Println("main线程退出")
}
结论:使用 go 协程后,执行的速度,比普通方法提高至少 4 倍
16.9 channel 使用细节和注意事项
- channel 可以声明为只读,或者只写性质 【案例演示】
- channel 只读和只写的最佳实践案例
- 使用 select 可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
"time"
)
func main() {
//使用select可以解决管道取数据的阻塞问题
//1、定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2、定义一个管道5个数据stirng
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "Hello" + fmt.Sprintf("%d", i)
}
//传统的方法再遍历管道时,如果不关闭会阻塞而导致deadlock
//问题,在实际开发中,可能我们不好确定什么时候关闭该管道
//可以用select方式解决
//label:
for {
select {
case v := <-intChan:
fmt.Printf("从intChan读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("从stringChan读取的数据%d\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到了,不玩了,程序员可以假如逻辑\n")
time.Sleep(time.Second)
return
//break label
}
}
}
select简介
1、Go的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前groutine。
2、select是Golang在语言层面提供的I/O多路复用的机制,其专门用来检测多个channel是否准备完毕:可读或可写。
3、select语句中除default外,每个case操作一个channel,要么读要么写
4、select语句中除default外,各case执行顺序是随机的
5、select语句中如果没有default语句,则会阻塞等待任一case
6、select语句中读操作要判断是否成功读取,关闭的channel也可以读取
- goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
在Golang中,if 后面可以紧着 并列 写出变量的定义,使用分号 “;” 和 if的表达式隔开。
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
func test() {
//这里我们可以使用 defer + recover
defer func() {
//捕获 test 抛出的 panic
if err := recover(); err != nil {//在Golang中,if 后面可以紧着 并列 写出变量的定义,使用分号 “;” 和 if的表达式隔开。
fmt.Println("test()发生错误!", err)
}
}()
//定义了一个map
var myMap map[int]string
myMap[0] = "golang" //error
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}