1.1 进程和线程说明:
1)进程就是程序在操作系统中的一次执行过程,是系统进行资源分配(CPU时间、内存等)和调度的基本单位;有独立的内存空间,切换开销大。
2) 线程是进程的一个执行实例/流,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。同一进程中的多线程共享内存空间,线程切换代价下;多线程通信方便;从内核层面来看线程其实是一种特殊的进程,它跟父进程共享了打开的文件和文件系统信息,共享了地址空间和信号处理函数。
3)一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行。
4)一个程序至少有一个进程,一个进程至少有一个线程
1.2 并发和并行
1)多线程程序(进程中含有多个执行实例/线程)在单核上运行,就是并发。
2)多线程程序在多核上运行,就是并行。
3)示意图:
并发的特点:
(1)多个任务/线程作用在一个CPU
(2)从微观角度,在一个时间点上其实只有一个任务/线程在执行;每个线程执行10ms,进行轮循操作处理机制。
并行的特点:
(1)多个任务作用在多个CPU上,有效利用了多核电脑的优势
(2)从微观角度,其本质上是在一个时间点上,同时执行多个任务/线程
(3)并行的运行速度更快
4)小结:
(1)在此介绍的并发和外部并发不是同一概念,外部并发是否同时完成线程任务取决于实际情况。
(2)相比其他语言,Go语言自带能实现多任务/线程并行处理机制,能够更有效地利用多核电脑的优势。这是Go语言的凸显有优势。
1.3 Go协程和Go主线程
1)Go主线程(有程序员直接称为线程/也可以理解为进程):一个Go线程上,可以起多个协程(轻易支持上万个协程),协程可以理解为:协程是轻量级的线程(编译器底层进行了优化,共享了相同信息,减轻了存储内存);
一个goroutine默认占用内存:2KB,而线程默认占用为8MB;
在切换层面:goroutine只涉及三个寄存器(PC/SP/DX)的值修改,而线程涉及模式切换(从用户态切换到内核态)、16个寄存器(PC/SP等)的刷新
2)Go协程的特点
(1)有独立的栈空间
(2)共享程序堆空间
(3)协程是轻量级的线程
(4)调度由用户控制
CPU上并发执行多个线程实质是对各个线程进行资源分配(执行时间),用户线程切换等无需内核态进行切换,有效利用内核态线程所分配的资源。
编写一个程序,完成如下功能:
1)在主线程(可以理解为进程)中,开启一个goroutine,该协程每隔一秒输出"hello,world"
2)在主线程中也隔一秒输出"hello,world",输出10次后,退出程序
3)要求主线程和goroutine同时执行
代码实现:
使用关键字go
,开辟一个协程
func test() {
for i := 1; i < 11; i++ {
// fmt.Printf("hello,world %v\n", i)
fmt.Println("test() hello,world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() //关键字go ,开辟一个协程
for i := 1; i < 11; i++ {
// fmt.Printf("hello,world %v\n", i)
fmt.Println("main() hello,world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
主线程和协程执行流程图:
快速入门小结:
1)主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源。
2)协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3)Golang的协程机制是Go语言的重要特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大。相较而言,突出了Golang在并发上的优势。
MPG模式基本介绍:
1)M:操作系统的主线程(物理线程/内核线程)
2)P:协程执行所需要的的上下文
3)G:协程
MPG模式运行的状态1:
1)当前有三个主线程M,如果该三个主线程都作用在一个CPU上,就是并发机制;若在不同的CPU上运作,则是并行机制。
2)M1,M2,M3,正在执行一个协程G,M1的协程队列有3个协程,M2的协程队列有2个协程,M3的协程队列有2个协程在等待。
3)从图中可知:Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程。
4)其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能就耗光CPU资源。
MPG模式运行的状态2:(动态运行)
1)分成两个部分来看
2)原来的运行情况(图左)是M0主线程正在执行G0协程,另外三个协程在队列等待。
3)如果G0协程阻塞,例如读取文件或数据库等
4)这是Golang就会创建M1主线程(也可能是从已有的线程池中取出M1),将等到中的三个协程挂到M1下开始执行,同时M0主线程下的G0协程仍然执行(文件的io的读写)。
5)这种MPG调度模式,既可以让G0协程执行,同时也不会让队列的其它协程一直阻塞,仍然能并发/并行执行。
6)等G0协程不阻塞了,M0会被放到空闲的主线程中继续执行(从已有的线程池中取出),同时G0协程又会被唤醒。
使用到runtime
包中的func NumCPU()
和func GOMAXPROCS()
函数
func NumCPU
func NumCPU() int
NumCPU返回本地机器的逻辑CPU个数。
func GOMAXPROCS
func GOMAXPROCS(n int) int
GOMAXPROCS设置可同时执行的最大CPU数,并返回先前的设置。 若 n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过 NumCPU 查询。本函数在调度程序优化后会去掉。
func main() {
cpuNum := runtime.NumCPU()
fmt.Println("cpunum =", cpuNum)
// 设置使用多个CPU
primeNum := runtime.GOMAXPROCS(cpuNum - 2)
fmt.Println("primeNum =", primeNum)
}
Output:
cpunum = 12
primeNum = 12
注:
go1.8后,默认让程序运行在多核上,可以不用设置了。
需求分析:
案例:
1)需求:计算1-200的各个数的阶乘,并且各个数的阶乘放入到map中
2)要求使用goroutine完成
var myMap = make(map[int]int, 10)
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res //fatal error: concurrent map writes
}
func main() {
for i := 1; i <= 200; i++ {
go test(i)
}
time.Sleep(10*time.Second)
// 显示结果
for i, v := range myMap {
fmt.Printf("map[%v]=%v\n", i, v)
}
}
Output:
fatal error: concurrent map writes
分析:
1)使用goroutine完成,效率高,但是会出现并发/并行安全问题。这是由于多个协程都在向同一个map进行写操作。
2)这就引出了不同的goroutine如何通信的问题
3)在运行某个程序时,欲知道是否存在资源竞争问题,在编译该程序时增加一个参数-race
即可
go build -race main.go
main.exeWARNING: DATA RACE
针对上述问题的解决方案:
1)定义全局变量加锁(互斥锁)同步
2)channel
使用全局变量加锁同步改进程序:
要是使用到sync
包中定义的定义的结构体Mutex
(互斥)的func (*Mutex) Lock
和func (*Mutex) Unlock
方法。
sync
包提供了基本的同步基元,如互斥锁。sync
包中函数\方法大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。
type Mutex
type Mutex struct {
// 包含隐藏或非导出字段
}
Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁
func (m *Mutex) Lock()
Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁。
func (m *MUnlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。utex) Unlock()
加锁同步改进程序goroutine工作机制示意图:
改进代码:
var (
myMap = make(map[int]int, 10)
// 声明一个全局的互斥锁
// 来源于sync包定义的Mutex结构体
// sync:synchronize 同步 Mutex:互斥
lock sync.Mutex
)
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
// 加锁
lock.Lock()
myMap[n] = res //fatal error: concurrent map writes
// 解锁
lock.Unlock()
}
func main() {
for i := 1; i <= 20; i++ {
go test(i)
}
// 阻塞程序10s,所有协程都完成
time.Sleep(10 * time.Second)
// 显示结果
// 读取map 时也需要上互斥锁
// 由于goroutine可能存在滞留,在读取文件时,同时存在写入操作,导致资源竞争
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%v]=%v\n", i, v)
}
lock.Unlock()
}
为什么需要Channel
前面使用全局变量加锁同步来解决goroutine的通讯并不完美,加锁操作适合于低水平程序线程,高水平同步使用channel通信更好些。
1)主线程在等待所有goroutine全部完成的时间很难确定
2)如果主线程休眠时间长了,加长了等待时间,资源浪费;若等待时间短了可能还有goroutine处于工作状态,这时主线程的退出会销毁协程
3)通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
4)因此引出了新的通讯机制-channel
channel介绍
1)channel本质就是一个数据结构-队列
2)数据先进先出(First in First out,FIFO)
3)管道本身线程安全, 多协程goroutine访问时,不需加锁。
4) channel是有类型的,一个string类型的channel只能存放string类型的数据。若要想一个管道同时存放多种数据类型,则设成成interface{}类型即可。(尽量不要混用类型,若混用调取数据还需断言操作)
2.1 定义/声明channel
var 变量名 chan 数据类型
,chan
为关键字。
举例:
var intChan chan int
,intChan为存放int数据的管道
var mapChan chan map[int]string
,mapChan存放map[int]string类型数据的管道
var perChan chan *Person
,perChan为存放Person结构体指针类型的管道
说明:
1)channel是引用类型
2)channel和map一样需初始化后才能写入数据,即需make
处理后才能使用
3)管道是有类型的,严格按照类型匹配
4)管道的容量是定死了的,不像map能动态增加;管道的工作机制是取出一个数据,其长度自动减小,容量不发生变化。因此,可以取出一个数据,再往管道里加入新的数据,实现类似动态增加的效果。
2.2 写入/读取管道数据
channel的写入和读取操作都很形象化;写/推入使用->
,读/推出使用<-
.
入门举例:
var intChan = make(chan int,3)
写入:intChan <- 10
;读出:num:= <- intChan
入门案例1:
func main() {
// 演示管道的使用
//1.创建一个可以存放三个int类型的管道
var intChan = make(chan int, 3)
// var intChan chan int
// intChan = make(chan int,3)
// 2.intChan(引用类型) 本身存放内容是地址
fmt.Printf("intChan=%v,intChan的地址是=%p\n", intChan, &intChan)
// intChan=0xc0000d4080,intChan的地址是=0xc0000ce018
// 3.向管道写入数据
intChan <- 10
num := 20
intChan <- num
intChan <- 30
// 注意,管道中写入/推入数据不能超过管道的容量
// intChan <- 40 //运行则会报错,
// fatal error: all goroutines are asleep - deadlock!
<-intChan
intChan <- 40
//运行成功,管道流动,取出一个后,在写入一个未超出容量
// 4.查看管道的长度和容量
fmt.Printf("intChan的len=[%v],cap=[%v]\n", len(intChan), cap(intChan))
// 从管道中读取数据
num1 := <-intChan
fmt.Println("num2=", num1)
fmt.Printf("intChan的len=[%v],cap=[%v]\n", len(intChan), cap(intChan)) //2,3
// 读出数据后管道的长度减一,容量不变
// 6.在没有使用goroutine的情况下,若管道数据已经全部取出,再取数据同样会报deadlock
num2 := <-intChan
num3 := <-intChan
fmt.Println("num2=", num2, "num3=", num3)
num4 := <-intChan
fmt.Println("num2=", num2, "num3=", num3, "num4", num4) //deadlock
}
入门案例2:
对于存放多种数据类型的管道,在提取某个数据,并编译使用该数据的字段或/方法时,需使用到类型断言。
type Cat struct {
Name string
Age int
}
func main() {
allChan := make(chan interface{}, 5)
cat1 := Cat{
Name: "tom",
Age: 5,
}
cat2 := Cat{"tom~", 4}
allChan <- 10
allChan <- "hello"
allChan <- cat1
allChan <- cat2
//由于管道的本质是队列,若想取出管道中的某个非首位数据,
// 需先将前的数据退出管道
<-allChan //直接退出,进入GC垃圾回收器
<-allChan
// 取得管道中原先第三个数据
cat11 := <-allChan
fmt.Printf("cat11类型=%T,cat11=%v\n", cat11, cat11)
// cat11类型=main.Cat,cat11={tom 5} 在运行层面 获知cat11的类型是Cat结构体
// fmt.Println("cat11的name:", cat11.Name) //报错
// (type interface{} has no field or method Name
//从编译层面,cat11仍然是interface{}类型,interface{}没有字段的
// 因此需要使用类型断言,即可编译通过
cat12, ok := cat11.(Cat)
if ok {
fmt.Println("cat1的name:", cat12.Name)
}
}
2.3 channel的关闭和遍历
2.3.1 channel的关闭
使用内置函数close
可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以读取channel中存在的数据。
func close(c chan<- Type)
内建函数close关闭通道,该通道必须为双向的或只发送的。它应当只由发送者执行,而不应由接收者执行,其效果是在最后发送的值被接收后停止该通道。在最后的值从已关闭的信道中被接收后,任何对其的接收操作都会无阻塞的成功。对于已关闭的信道,语句
x, ok := <-c
还会将ok置为false。
案例演示:
unc main() {
intChan := make(chan int, 3)
intChan <- 10
intChan <- 20
// 关闭通道,使用内置函数close
close(intChan)
// 此时无法再对通道写输入数据
// intChan <- 30 //报错:panic: send on closed channel
// 当通道关闭后可以取其存在排队的值
num1 := <-intChan
num2 := <-intChan
fmt.Println("num1=", num1, "num2=", num2) //num1= 10 num2= 20
// 当最后值从已关闭的通道中被接收后,任何对其接收操作都会无阻塞成功
num3 := <-intChan
fmt.Println("num3=", num3) // num3= 0
// 但对于空值关闭通道的的接收操作,其判断变量还是会置为false
num4, ok := <-intChan
if !ok {
fmt.Println("channel is closed and empty")
fmt.Println("num4=default value:", num4)
}
Output:
num1= 10 num2= 20
num3= 0
channel is closed and empty
num4=default value: 0
2.3.2 channel的遍历
channel支持for-range
的方式进行遍历,由于channel的长度是在改变的使用传统的for
遍历就显得不合时宜。
需注意的细节:
1)在遍历时,如果channel没有关闭,则会出现deadlock的错误。这是由于在管道没有关闭情况下,使用for-range
遍历,当遍历完管道所有数据后,底层运行仍然在死等下一个内容,不会主动退出。
2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出。
func main() {
intChan := make(chan int, 50)
// 向管道中写入数据
for i := 0; i < 50; i++ {
intChan <- i * 2 //写入50个偶数
}
// 由于管道本身进行一次读操作后自身长度会减一
// 1.遍历管道使用传统的for循环不合适
intSlice := make([]int, 50)
for i := 0; i < len(intChan); i++ {
intSlice[i] = <-intChan
}
fmt.Println(intSlice)
// 输出结果,只遍历了管道的前一半数据
// 改进方法:
// a := len(intChan)
// for i := 0; i < a; i++ {
// intSlice[i] = <-intChan
// }
// fmt.Println(intSlice)
// 使用传统的for方法遍历 不需关闭通道,因其限制死了遍历的次数。
//2. 使用for-range 进行遍历
// 若没有关闭管道,在遍历完所有管道内容后,底层编译会依然死等下一个内容,不会退出
// 故会报错:fatal error: all goroutines are asleep - deadlock!
close(intChan)
// channel 本质上是队列,故其使用for-range遍历时只返回值,不存在下标
// channel 的推出/读出 严格按照先进先出FIFO
for v := range intChan {
fmt.Println("v=", v)
}
}
2.3.3 channel遍历和关闭案例小结
1)案例中对管道的遍历,就是等价于从管道中取数据,即:<- ch
2)注意需要close管道,否则就会出现deadlock
3)在for-range 管道时,当遍历到最后的时候,发现管道关闭,就结束读取数据的工作,正常退出。
4)在for-range管道时,当遍历到最后,发现管道没有关闭,程序会认为还有数据继续写入,因此会等待,如果程序持续没有数据写入们就会出现死锁deadlock。
2.4 goroutine和channel应用案列
案列一:
使用同一通道完成读写操作
func ReadData(c1 chan int, c2 chan bool) {
for {
v, ok := <-c1
if !ok {
break
}
fmt.Println("v=", v)
}
c2 <- true
close(c2)
}
func main() {
// 定义两个管道
// 管道1 用于读写
intChan := make(chan int, 25)
// 管道2 用于判断读操作结束
exitChan := make(chan bool, 1)
t1 := time.Now()
//write data
go func(c1 chan int) {
for i := 0; i < 25; i++ {
c1 <- i * 2
fmt.Println("write v=", i*2)
}
close(c1)
}(intChan)
// read data
go ReadData(intChan, exitChan)
t2 := time.Now()
fmt.Printf("goroutine consume time=%v\n", t2.Sub(t1)) //0s
// 当exitChan管道中读取到数据,则读取操作结束
for {
if _, ok := <-exitChan; ok {
break
}
}
t3 := time.Now()
fmt.Println("读写操作完成,共耗时=", t3.Sub(t1))
}
思路示意图:
应用案列2:阻塞
1)注销上述案例一代码go readData(intChan,exitChan)
,并将intChan
的容量改为10,则再向管道写入数据25个数据就会阻塞,造成deadlock现象。
2)当管道容量小于预计写入数据量时,具有相应的读操作协程,管道也不会发生无意义堵塞;并且Golang支持异步操作:支持写入管道频率和读管道频率不一致,底层编译器会产生有意义的堵塞,并不会发生死锁现象。
代码:
func ReadData(c1 chan int, c2 chan bool) {
for {
v, ok := <-c1
if !ok {
break
}
// 每次读出操作间隔一秒,而写入操作是一次性写入的
// 证明Golang支持异步操作
time.Sleep(time.Second)
fmt.Println("v=", v)
}
c2 <- true
close(c2)
}
func main() {
// 定义两个管道
// 管道1 用于读写
intChan := make(chan int, 10)
// 管道2 用于判断读操作结束
exitChan := make(chan bool, 1)
t1 := time.Now()
//write data,管道容量小于写入数据量
go func(c1 chan int) {
for i := 0; i < 25; i++ {
c1 <- i * 2
fmt.Println("write v=", i*2)
}
close(c1)
}(intChan)
// read data
go ReadData(intChan, exitChan)
// go 调用后会直接向下执行代码块,故t2.Sub(t1)毫无意义
//t2 := time.Now()
//fmt.Printf("goroutine consume time=%v\n", t2.Sub(t1)) //0s
// 当exitChan管道中读取到数据,则读取操作结束
for {
if _, ok := <-exitChan; ok {
break
}
}
t3 := time.Now()
fmt.Println("读写操作完成,共耗时=", t3.Sub(t1))
}
应用案列3:统计素数
需求:统计1-100000的数字中,哪些是素数?
思路分析:
1)传统的方法,就是一个循环,循环的判断各个数是不是素数就ok了
代码:
//要求打印100以内的素数,每行显示5个,最后素数的和
func primeNumber(n1 int) int {
count := 0
for i := 2; i <= n1-1; i++ {
count++
if n1%i == 0 { //不是素数
break
}
}
if count == n1-2 {
return n1
} else {
return 0
}
}
func main() {
sum := 0
count := 0
for i := 1; i <= 100; i++ {
a := primeNumber(i)
if a == 0 {
continue
} else {
count++
sum += i
if count%5 != 0 {
fmt.Printf("%d\t", i)
} else {
fmt.Printf("%d\r\n", i) //每行显示第五个换行
}
}
}
fmt.Println(sum)
}
2)使用并发/并行的方式,将统计素数的任务分配给多个(4)个goroutine去完成,完成任务时间更短。
(分析思路):
代码:
// 需求:统计1-100000的数字中,哪些是素数?
//并将这些素数排序好后写入文件中
// 开启判断素数协程
// 要求,是素数就放入到primeChan管道中
func PrimeNum(c1, c2 chan int, c3 chan bool) {
for {
v, ok := <-c1
// time.Sleep(time.Millisecond * 10)
if !ok {
break
}
flag := true //标识是否为素数
for i := 2; i <= v-1; i++ {
if v%i == 0 {
flag = false
break
}
}
if flag {
c2 <- v //是素数就写入primeChan
} else {
continue //不是素数继续循环
}
}
//有一个协程取不到数据退出了
// 向退出标识管道exitChan写入一个标识
fmt.Println("有一个primeNum协程取不到数据,退出")
c3 <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 20000) //放入结果
exitChan := make(chan bool, 4) //标识退出的管道
// 开启一个协程向intChan放入1-8000个数
go func(c1 chan int) {
for i := 1; i <= 100000; i++ {
c1 <- i
}
close(c1) //读完数据后,关闭管道intChan
}(intChan)
// 开辟四个primeNum协程
for i := 0; i < 4; i++ {
go PrimeNum(intChan, primeChan, exitChan)
}
//开辟一个协程用于判断exitChan是否写入4个值,
// 即素数判断协程们是否都关闭了,并关闭primeChan
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
close(primeChan)
}()
// 将得出的素数进行排序,然后写进文件
// 定义一个文件写入完标识,用于阻塞主线程
Flag := false
go func(c2 chan int) {
intSlice := make([]int, 0) //存放素数
for {
v, ok := <-c2
if !ok {
break
}
intSlice = append(intSlice, v)
}
// 进行升序排序
sort.Ints(intSlice)
// 写入文件处理:
// 打开/创建一个文件
filePath := "E:\\goproject\\src\\go_code\\chapter15\\ChannelPrimeNumber\\Channel_Primer\\素数.txt"
// 加入os.O_TRUNC 每次运行都会重新清除文件上传写入内容
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
log.Fatal(err)
}
// 创建带缓存的writer
writer := bufio.NewWriter(file)
for _, v := range intSlice {
str := strconv.Itoa(v) + "\n"
writer.WriteString(str)
}
writer.Flush()
Flag = true
}(primeChan)
// 主线程阻塞处理
for {
if Flag {
break
}
}
fmt.Println("操作完成")
}
小结:使用4个协程并行处理耗时会比传统方法快4倍以上。
2.5 channel 使用细节和注意事项
1)channel默认为双向通道,可以在申明时直接确定其只读或只写性质
// 1.声明只写的管道
// 写法一:
var intChan1 chan<- int
intChan1 = make(chan int, 10)
intChan1 <- 10
// num1 := <-intChan1 //报错,cannot receive from send-only channel
// 写法二:
intChan3 := make(chan<- int, 10)
// num2 := <-intChan3//报错,cannot receive from send-only channel
for i := 0; i < 10; i++ {
intChan3 <- i
if i == 9 {
fmt.Println("写入完成")
}
}
//2.声明只读的管道
// 写法一:
var intChan2 <-chan int
intChan2 = make(chan int, 10)
fmt.Println(intChan2)
// intChan2 <- 10 //报错 cannot send to receive-only type <-chan int
// 写法二:
intChan4 := make(<-chan int, 10)
fmt.Println(intChan4)
// 单独申明只读管道无实际意义
2)也可以对已经创建的双向通道,在调用函数/方法时,对该双向通道进行只读或只写性质的定义。
// 3.双向管道,单向管道的综合应用
c := make(chan int, 10)
exitChan := make(chan struct{}, 2)
//只能写管道,seed-only
go func(c1 chan<- int, c2 chan struct{}) {
for i := 0; i < 10; i++ {
c1 <- i
}
close(c1)
var a struct{}
exitChan <- a
}(c, exitChan)
// 只能读管道 receive-only
go func(c1 <-chan int, c2 chan struct{}) {
for {
v, ok := <-c1
if !ok {
break
}
fmt.Println(v)
}
var a struct{}
exitChan <- a
}(c, exitChan)
total := 0
for _ = range exitChan { //用于判断exitChan中是否写两个数据
total++
if total == 2 {
break
}
}
fmt.Println("finish work")
}
3)使用select
可以解决从管道取数据的阻塞问题。
//传统的方法在遍历时不关闭管道就会产生死锁deadlock
// 问题:在实际开发中,可能不好确定何时关闭管道
// 针对上述问题,可以送select方式解决
slecet
的使用机制:
(1)使用select,case中的管道一直未关闭,切且不会发生deadlock
(2) 当一个case中的值取不到时,会自动匹配下一个case
(3) 当所有case都不匹配时,执行default项
// label:
for {
select {
case v := <-intChan:
...
case v := <-stringChan:
...
......
default:
.....
return
// break label
}
}
案例代码:
func main() {
// 使用`select`可以解决从管道取数据的阻塞问题。
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
// label:
for {
// 使用select,case中的管道一直未关闭,切且不会发生deadlock
// 当一个case中的值取不到时,会自动匹配下一个case
// 当所有case都不匹配时,执行default项
select {
case v := <-intChan:
time.Sleep(time.Second)
fmt.Printf("从intChan中取得值%d\n", v)
case v := <-stringChan:
time.Sleep(time.Second)
fmt.Printf("从stringChan中取得值%s\n", v)
default:
time.Sleep(time.Second)
fmt.Println("操作完成,该处可加入业务逻辑")
return
// break label
}
}
}
4)goroutine中使用defer+recover
,解决协程中出现的panic
导致程序崩溃的问题。
defer
操作参考:defer说明
defer+recover
操作参考:错误处理机制
问题说明:为了解决协程运行中可能出现panic
,造成整个程序崩溃的问题;采用defer+recover
错误处理机制,之这样即使某个协程发生了问题,但是主线程序和其它协程不受影响,可以继续执行。
入门案例代码演示:
// goroutine中使用`defer + recover`,解决协程中出现的`panic`导致程序崩溃的问题。
func sayHello(c1 chan string) {
for i := 0; i < 10; i++ {
c1 <- "hello" + fmt.Sprintf("%d", i)
}
close(c1)
}
func test() {
// 需注意:defer + recover 错误处理机制必须放在代码块开头/错误可能发生前面
defer func() {
// 捕获test抛出的panic
err := recover()
if err != nil {
// 伪代码
fmt.Println("test()函数报错,发送给xxx.com")
}
}()
var myMap map[int]string
myMap[0] = "err" //myMap未make error
}
func main() {
stringChan := make(chan string, 10)
go sayHello(stringChan)
go test()
for i := 0; i < 10; i++ {
<-stringChan
}
fmt.Println("测试完成")
}
案例小结:defer + recover
错误处理机制必须 放在代码块开头/错误可能发生前面