Golang语言的核心特色
- Goroutine
- 基本介绍
- 进程和线程介绍
- 程序、进程和线程的关系示意图
- 并发和并行
- Go协程和Go主线程
- 快速入门
- 案例说明
- 小结
- goroutine的调度模型
- MPG模式运行的状态 -1
- MPG模式运行的状态 - 2
- 设置Go运行的CPU数
- 基本介绍
- Channel(管道)
- 看个需求
- 不同goroutine之间如何通讯
- 使用全局变量加锁同步改进程序
- 为什么需要channel
- channel的基本介绍
- 定义/声明channel
- 管道的初始化、写入数据到管道、从管道读取数据
- channel使用的注意事项
- 读写channel案例演示
- channel的遍历和关闭
- channel的关闭
- channel的遍历
- channel遍历和关闭的案例演示
- 应用案例
- 应用案例-利于管道实现边写边读
- 应用案例 - 阻塞
- 应用案例-求素数
- channel使用细节和注意事项
- channel可以声明为只读,或者只写性质
- channel只读和只写的最佳实践案例
- 使用select可以解决从管道取数据的阻塞问题
- goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
- 管道的练习题
Goroutine
基本介绍
进程和线程介绍
-
进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
-
线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
-
一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
-
一个程序至少有一个进程,一个进程至少有一个线程
程序、进程和线程的关系示意图
并发和并行
-
多线程程序在单核上运行,就是并发
-
多线程程序在多核上运行,就是并行
并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
Go协程和Go主线程
Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,可以这样理解:协程是轻量级的线程【编译器做优化】
Go协程的特点
1) 有独立的栈空间
2) 共享程序堆空间
3) 调度由用户控制
快速入门
案例说明
编写一个程序,完成如下功能:
1) 在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔一秒输出“hello,world”
2) 在主线程中也每隔一秒输出“hello,world”,输出10次后,退出程序
3) 要求主线程和goroutine同时执行
import (
"fmt"
"strconv"
"time"
)
//编写一个函数/每隔一秒输出"hello,world"
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() hello,world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() //开启了一个协程
for i := 1; i <= 10; i++ {
fmt.Println("main() hello,world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
//main() hello,world1 //main主线程和test协程同时执行
//test() hello,world1
//main() hello,world2
//test() hello,world2
//......
小结
-
主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源
-
协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
-
Go的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就突显了Go在并发上的优势了
goroutine的调度模型
MPG模式运行的状态 -1
-
当前程序有三个M,如果三个M都在一个cpu上运行,就是并发,如果在不同的cpu上运行就是并行
-
M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有三个,M3的协程队列有两个
-
从上图可以看到:Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程
-
其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu
MPG模式运行的状态 - 2
-
分成两个部分来看
-
原来的情况是MO主线程正在执行Go协程,另外有三个协程在队列等待
-
如果Go协程阻塞,比如读取文件或者数据库等
-
这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的Go仍然执行文件io的读写
-
这样的MPG调度模式,可以既让Go执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行
-
等到Go不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时Go又会被唤醒
设置Go运行的CPU数
为了充分利用多cpu的优势,在Go程序中,设置运行的cpu数目
import (
"fmt"
"runtime"
)
func main() {
//获取当前系统cpu的数目
num := runtime.NumCPU()
//这里设置num - 1的cpu运行Go程序
runtime.GOMAXPROCS(num - 1)
fmt.Println("num = ", num)
}
Go1.8后,默认让程序运行在多核上,可以不用设置
Go1.8前,还是要设置一下,可以更高效的利用cpu
Channel(管道)
看个需求
需求:现在要计算1 - 200 的各个数的阶乘,并且把各个数的阶乘放入到map中,最后显示出来
要求:使用goroutine
分析思路
1) 使用goroutine来完成,效率高,但是会出现并发/并行安全问题
2) 这里就提出了不同goroutine如何通信的问题
代码区
1) 使用goroutine来完成(看看使用goroutine并发完成会出现什么问题?然后再去解决)
2) 在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单,在编译该程序时,增加一个参数 - race 即可
import (
"fmt"
"time"
)
//思路
//1. 编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的协程多个,统计的结果放入到map中
//3. map应该做出一个全局的
var (
myMap = make(map[int]int,10)
)
//test函数就是计算n!,将这个结果放入到myMap
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)
}
}
//fatal error: concurrent map writes
//
//goroutine 55 [running]:
//runtime.throw(0x4d6d6d, 0x15)
// E:/GO/go/src/runtime/panic.go:774 +0x79 fp=0xc0000eff60 sp=0xc0000eff30 pc=0x42d229
//runtime.mapassign_fast64(0x4b6240, 0xc00005c330, 0x31, 0x0)
// E:/GO/go/src/runtime/map_fast64.go:101 +0x357 fp=0xc0000effa0 sp=0xc0000eff60 pc=0x410167
//main.test(0x31)
// E:/gostudent/src/2020-04-06/main.go:21 +0x6b fp=0xc0000effd8 sp=0xc0000effa0 pc=0x49c72b
//runtime.goexit()
// E:/GO/go/src/runtime/asm_amd64.s:1357 +0x1 fp=0xc0000effe0 sp=0xc0000effd8 pc=0x4556a1
//created by main.main
// E:/gostudent/src/2020-04-06/main.go:26 +0x5f
//
//goroutine 1 [runnable]:
//time.Sleep(0x2540be400)
// E:/GO/go/src/runtime/time.go:84 +0x248
//main.main()
// E:/gostudent/src/2020-04-06/main.go:29 +0x82
不同goroutine之间如何通讯
-
全局变量的互斥锁
-
使用管道channel来解决
使用全局变量加锁同步改进程序
因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes
解决方案:加入互斥锁
数的阶乘很大,结果会越界,可以将求阶乘改成sum += uint64(i)
代码区改进
package main
import (
"fmt"
"sync"
"time"
)
//思路
//1. 编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的协程多个,统计的结果放入到map中
//3. map应该做出一个全局的
var (
myMap = make(map[uint]uint,10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁
//sync 是包:synchornized 同步
//Mutex :是互斥
lock sync.Mutex
)
//test函数就是计算n!,将这个结果放入到myMap
func test(n uint) {
var res uint = 1
var i uint = 1
for ; i <= n; i++ {
res *= i
}
//这里将res 放入到myMap
//加锁
lock.Lock()
myMap[n] = res // concurrent map writes?
//解锁
lock.Unlock()
}
func main() {
//这里开启多个协程完成这个任务[200个]
var i uint = 1
for ; 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()
}
需求注意的是:uint64最大到20的阶乘,大整数可以使用math/big 来进行 实例:https://blog.csdn.net/hudmhacker/article/details/90081630
为什么需要channel
-
前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
-
主线程在等待所有gorountine全部完成的时间很难确定,这里设置了10秒,仅仅是估算
-
如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
-
通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
-
上面种种分析都在呼唤一个新的通讯机制 - channel
channel的基本介绍
-
channel本质就是一个数据结构 - 队列
-
数据是先进先出【FIFO :first int first out】
-
线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
定义/声明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
....
说明
1) channel是引用类型
2) channel必须初始化才能写入数据,即make后才能使用
3) 管道是有类型的,intChan只能写入整数int
管道的初始化、写入数据到管道、从管道读取数据
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. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- 50
//intChan <- 99 //当给管道写入数据时,不能超过其容量
//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))
//6. 在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报告deadlock
num3 := <- intChan
num4 := <- intChan
num5 := <- intChan
fmt.Printf("num3 = %v num4 = %v num5 = %v ", num3, num4, num5)
}
//fatal error: all goroutines are asleep - deadlock!
//intChan 的值 = 0xc000090000 intChan本身的地址 = 0xc00008a018
//channel len = 3 cap = 3
//num2 = 10
//channel len = 2 cap = 3
//
//goroutine 1 [chan receive]:
//main.main()
// E:/gostudent/src/2020-04-06/main.go:28 +0x4d4
channel使用的注意事项
-
channel 中只能存放指定的数据类型
-
channel 的数据放满后,就不能再放入了
-
如果从channel取出数据后,可以继续放入
-
在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
读写channel案例演示
- 创建一个intChan,最多可以存放3个int,演示存3个数据到intChan,然后再取出这三个int
func main() {
var intChan chan int
intChan = make(chan int, 3)
intChan <- 10
intChan <- 20
intChan <- 10
//因为intChan 的容量为3,再存放会报告deadlock
//intChan <- 50
num1 := <- intChan
num2 := <- intChan
num3 := <- intChan
//因为intChan 这时已经没有数据了,再取会报告deadlock
//num4 := <- intChan
fmt.Printf("num1 = %v num2 = %v num3 = %v", num1, num2, num3)
}
//num1 = 10 num2 = 20 num3 = 10
- 创建一个mapChan,最多可以存放10个map[string]string的key-val,演示写入和读取
func main() {
var mapChan chan map[string]string
mapChan = make(chan map[string]string, 2)
m1 := make(map[string]string, 2)
m1["city1"] = "北京"
m1["city2"] = "天津"
m2 := make(map[string]string, 2)
m2["hero1"] = "宋江"
m2["hero2"] = "林冲"
mapChan <- m1
mapChan <- m2
num1 := <- mapChan
num2 := <- mapChan
fmt.Printf("num1 = %v num2 = %v", num1, num2)
}
//num1 = map[city1:北京 city2:天津] num2 = map[hero1:宋江 hero2:林冲]
- 创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和读取的用法
type Cat struct{
Name string
Age int
}
func main() {
var catChan chan Cat
catChan = make(chan Cat, 10)
cat1 := Cat{Name: "tom", Age: 18,}
cat2 := Cat{Name: "zise", Age: 18,}
catChan <- cat1
catChan <- cat2
//取出
cat11 := <- catChan
cat22 := <- catChan
fmt.Println(cat11, cat22)
}
//{tom 18} {zise 18}
- 创建一个catChan2,最多可以存放10个*Cat变量,演示写入和读取的用法
type Cat struct{
Name string
Age int
}
func main() {
var catChan chan *Cat
catChan = make(chan *Cat, 10)
cat1 := Cat{Name: "tom", Age: 18,}
cat2 := Cat{Name: "zise", Age: 18,}
catChan <- &cat1
catChan <- &cat2
//取出
cat11 := <- catChan
cat22 := <- catChan
fmt.Println(*cat11, *cat22)
}
//{tom 18} {zise 18}
- 创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法
type Cat struct {
Name string
Age int
}
func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)
cat1 := Cat{Name: "tom", Age: 18}
cat2 := Cat{Name: "zise", Age: 18}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"
//取出
cat11 := <- allChan
cat22 := <- allChan
v1 := <- allChan
v2 := <- allChan
fmt.Println(cat11, cat22, v1, v2)
}
//{tom 18} {zise 18} 10 jack
- 看下面的代码,会输出什么
type Cat struct {
Name string
Age int
}
func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)
cat1 := Cat{Name: "tom", Age: 18}
cat2 := Cat{Name: "zise", Age: 18}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"
//取出
//cat11 := <- allChan
//fmt.Println(cat11.Name)
// # command-line-arguments
//src\go_code\chapter15\exec03\test03.go:23:19: cat11.Name undefined (type interface {} is interface with no methods)
newCat := <- allChan //从管道中取出的Cat是什么
fmt.Printf("newCat = %T newCat = %v \n", newCat, newCat)
//下面写法是错误的,编译不通过
//fmt.Printf("newCat.Name = %v", newCat.Name)
//使用类型断言
a := newCat.(Cat)
fmt.Printf("newCat.Name = %v", a.Name)
}
//newCat = main.Cat newCat = {tom 18}
//newCat.Name = tom
channel的遍历和关闭
channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
close(intChan) //close
//这时不能够再写入数到channel
//intChan <- 300
fmt.Println("oko")
//当管道关闭后,读取数据是可以的
n1 := <- intChan
fmt.Println("n1 = ", n1)
}
//oko
//n1 = 100
channel的遍历
channel支持 for - range 的方式进行遍历,注意两个细节
-
在遍历时,如果channel没有关闭,则会出现deadlock的错误
-
在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
channel遍历和关闭的案例演示
func main() {
//遍历管道
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan2 <- i *2 //放入100个数据到管道
}
//遍历管道不能使用普通的for循环
//for i := 0; i < len(intChan2); i++ {
//
//}
//1)在遍历时,如果channel没有关闭,则会出现deadlock的错误
//2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
close(intChan2)
for v := range intChan2 {
fmt.Println("v = ", v)
}
}
应用案例
应用案例-利于管道实现边写边读
请完成goroutine和channel协同工作的案例,具体要求:
-
开启一个writeData协程,向管道intChan中写入50个整数
-
开启一个readData协程,从管道intChan中读取writeData写入的数据
-
注意:writeData和readData操作的是同一个管道
-
主线程需要等待writeData和readData协程都完成工作才能退出【管道】
import (
"fmt"
"time"
)
//writeData
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
//放入数据
intChan <- i
fmt.Println("writeData", i)
time.Sleep(time.Second)
}
close(intChan) //关闭
}
//readData
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
}
}
}
var (
myMap = make(map[int]int, 10)
)
func cal(n int) map[int]int {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res
return myMap
}
func write(myChan chan map[int]int) {
for i := 0; i <= 15; i++ {
myChan <- cal(i)
fmt.Println("writer data:", cal(i))
}
close(myChan)
}
func read(myChan chan map[int]int, exitChan chan bool) {
for {
v, ok := <-myChan
if !ok {
break
}
fmt.Println("read data:", v)
}
exitChan <- true
close(exitChan)
}
func main() {
var myChan chan map[int]int
myChan = make(chan map[int]int, 20)
var exitChan chan bool
exitChan = make(chan bool, 1)
go write(myChan)
go read(myChan, exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
应用案例 - 阻塞
思考:假设我们注销掉go read(myChan,exitChan)会发生什么呢?
也就是说,只有写入myChan而没有读取myChan,当存入myChan里面的数据达到了myChan的容量,再继续存入就会报deadlock错误。同时,由于exitChan需要写入一个true,而exitChan需要读取完myChan中的数据后才写入一个true,但是现在不能进行读取,也就是说,true不会写入exitChan,就形成了阻塞。假设我们打开go read(myChan,exitChan),我们设置其每隔1秒才读取一条数据,而写入则让其正常运行,也就是说,写入很快,读取很慢,这样会导致deadlock吗?答案是不会,只要有读取,golang会有个机制,不会让myChan存储的值超过myChan的容量。
应用案例-求素数
需求
要求统计 1 - 8000的数字中,哪些是素数?
现在具备了goroutine和channel的知识后,就可以完成了
分析思路
传统的方法:使用一个循环,循环的判断各个数是不是素数
使用并发/并行的方式:将统计素数的任务分配给多个(4个)goroutine去完成,完成任务时间短
说明:有五个协程,三个管道。其中一个协程用于写入数字到intChan管道中,另外四个用于取出intChan管道中的数字并判断是否是素数,然后将素数写入到primeChan管道中,最后如果后面四个协程哪一个工作完了,就写入一个true到exit管道中,最后利用循环判断这四个协程是否都完成任务,并退出
package main
import (
"fmt"
"time"
)
//向intChan放入1 - 8000个数
func putNum(intChan chan int) {
for i:= 1; i <= 8000; i++ {
intChan <- i
}
//关闭intChan
close(intChan)
}
//从intChan取出数据,并判断是否为素数,如果是,就放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
//使用for循环
//var num int
var flag bool
for {
time.Sleep(time.Millisecond * 10)
num, ok := <- intChan
if !ok { //intChan 娶不到..
break
}
flag = true //假设是素数
//判断num是不是素数
for i := 2; i < num; i++ {
if num % i == 0 { //说明该num 不是素数
flag = false
break
}
}
if flag {
//将这个数就放入到primeChan
primeChan <- num
}
}
fmt.Println("有一个primeNum协程因为取不到数据,退出")
//这里还不能关闭primeChan
//向exitChan 写入true
exitChan <- true
}
func main() {
intChan := make(chan int, 200000)
primeChan := make(chan int, 200000) //放入结果
//标识退出的管道
exitChan := make(chan bool, 4) // 4个
//开启一个协程,向intChan放入1 - 200000个数
go putNum(intChan)
//开启四个协程,从intChan取出数据,
//并判断是否为素数,如果是,就放入到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//这里对主线程,进行处理
go func() {
for i := 0; i < 4; i++ {
<- exitChan
}
//当从exitChan 取出4个结果
//就可以关闭prprimeChan
close(primeChan)
}()
//遍历primeChan,把结果取出
for {
res, ok := <- primeChan
if !ok {
break
}
//将结果输出
fmt.Printf("素数 = %d\n", res)
}
fmt.Println("main线程退出")
}
升级
package main
import (
"fmt"
"time"
)
func isPrime(n int) bool {
for i := 2; i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}
//传统方法耗时
func Test() {
start := time.Now()
for i := 1; i < 80000; i++ {
isPrime(i)
}
cost := time.Since(start)
fmt.Printf("传统方法消耗时间为:%s", cost)
}
//向intChan放入1 - 80000个数
func putNum(intChan chan int) {
for i:= 1; i <= 80000; i++ {
intChan <- i
}
//关闭intChan
close(intChan)
}
//从intChan取出数据,并判断是否为素数,如果是,就放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
//使用for循环
//var num int
//var flag bool
for {
//time.Sleep(time.Millisecond * 10)
num, ok := <- intChan
if !ok { //intChan 娶不到..
break
}
//flag = true //假设是素数
//判断num是不是素数
// for i := 2; i < num; i++ {
// if num % i == 0 { //说明该num 不是素数
// flag = false
// break
// }
// }
// if flag {
// //将这个数就放入到primeChan
// primeChan <- num
// }
//}
isp := isPrime(num)
if !isp {
continue
} else {
primeChan <- num
}
}
fmt.Println("有一个primeNum协程因为取不到数据,退出")
//这里还不能关闭primeChan
//向exitChan 写入true
exitChan <- true
}
func main() {
intChan := make(chan int, 200000)
primeChan := make(chan int, 200000) //放入结果
//标识退出的管道
exitChan := make(chan bool, 4) // 4个
//记录当前时间
start := time.Now()
//开启一个协程,向intChan放入1 - 200000个数
go putNum(intChan)
//开启四个协程,从intChan取出数据,
//并判断是否为素数,如果是,就放入到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//这里对主线程,进行处理
go func() {
for i := 0; i < 4; i++ {
<- exitChan
}
//当从exitChan 取出4个结果
//就可以关闭prprimeChan
//计算耗时时间
cost := time.Since(start)
fmt.Printf("使用协程耗费时间:%s\n", cost)
close(primeChan)
}()
//遍历primeChan,把结果取出
for {
_, ok := <- primeChan
if !ok {
break
}
//将结果输出
//fmt.Printf("素数 = %d\n", res)
}
fmt.Println("main线程退出")
Test()
}
//有一个primeNum协程因为取不到数据,退出
//有一个primeNum协程因为取不到数据,退出
//有一个primeNum协程因为取不到数据,退出
//有一个primeNum协程因为取不到数据,退出
//使用协程耗费时间:876.6558ms
//main线程退出
//传统方法消耗时间为:3.3300976s
channel使用细节和注意事项
channel可以声明为只读,或者只写性质
func main() {
//管道可以声明为只读或者只写
//1. 在默认情况下,管道是双向
//var chan1 chan int //可读可写
//2. 声明为只写
var chan2 chan <- int
chan2 = make(chan int, 3)
chan2 <- 20
//num := <- chan2 //error
fmt.Println("chan2 = ", chan2)
//3. 声明为只读
var chan3 <- chan int
num2 := <- chan3
//chan3 <- 30 //err
fmt.Println("num2", num2)
}
channel只读和只写的最佳实践案例
//ch chan <- int 这样ch就只能写操作了
func send(ch chan <- int, exitChan chan struct{}) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
var a struct{}
exitChan <- a
}
//ch <- chan int ,这样ch 就只能读操作了
func recv(ch <- chan int, exitChan chan struct{}) {
for {
v, ok := <- ch
if !ok {
break
}
fmt.Println(v)
}
var a struct{}
exitChan <- a
}
func main() {
var ch chan int
ch = make(chan int, 10)
exitChan := make(chan struct{}, 2)
go send(ch, exitChan)
go recv(ch, exitChan)
var total = 0
for _ = range exitChan {
total ++
if total == 2 {
break
}
}
fmt.Println("结束...")
}
使用select可以解决从管道取数据的阻塞问题
import (
"fmt"
"time"
)
func main() {
//使用select可以解决从管道取数据的阻塞问题
//1. 定义一个管道10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2. 定义一个管道5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭会阻塞而导致deadlock
//问题:在实际开发中,可能不好确定什么时间关闭该管道
//可以使用select方式解决
//label:
for {
select {
//注意:这里intChan一直没有关闭,不会一直阻塞而deadlock
//会自动到下一个case匹配
case v := <-intChan:
fmt.Printf("从intChan读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("从stringChan读取的数据%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到了,不玩了,程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
//break label
}
}
}
goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
说明:如果起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic,进行处理,这样即使这个协程发生了问题,但是主线程仍然不受影响,可以继续执行。
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 {
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)
}
}
//main() ok= 0
//test() 发生错误 assignment to entry in nil map
//hello,world
//main() ok= 1
//hello,world
//main() ok= 2
//hello,world
//main() ok= 3
//hello,world
//main() ok= 4
//hello,world
//main() ok= 5
//hello,world
//main() ok= 6
//hello,world
//main() ok= 7
//hello,world
//main() ok= 8
//hello,world
//main() ok= 9
//hello,world
管道的练习题
说明:
-
创建一个Person结构体[Name,Age,Address]
-
使用rand方法配合随机创建10个Person实例,并放入到channel中
-
遍历channel,将各个Person实例的信息显示在终端...