Golang 语言之goroutine(协程)channel(管道)
协程引入
需求: 要求统计1-20000的数字中,哪些是素数
分析思路
1)传统的方法,就是使用一个循环,循环的判断各个数是不是素数
2)使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成,这时使用goroutine
基本介绍
进程和线程说明
1)进程就是程序在操作系统中的一次执行过程,使系统进行资源分配和调度的基本单位
2)线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
3)一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
4)一个程序至少有一个进程,一个进程知识有一个线程

并发和并行
1)多线程程序在单核上运行,就是并发
2)多线程程序在多核上运行,就是并行
示例图
并发: 因为一个CPU上。比如有10个线程,每个线程执行10 微秒(进行轮询操作),从人的角度看,好像10个线程都在运行,但是从微观上看,在某个时间点看,其实只有一个线程在运行,这就是并发

并行:因为是多个CPU上(10核CPU),例如10个线程,,每个线程执行10微秒(各自在不同CPU运行),从个人角度,10个线程都在运行,但从微观上看,在某个时间点看,10个进程也是同时执行,这就是并行

goroutine 基本介绍
Go 协程和Go 主线程
1)Go主线程(有程序员直接程为线程/也可以理解进程):一个Go线程上,可以起多个协程,可以这样理解,协程是轻量级的线程【编译器做的优化】
2)Go协程的特点
有独立的栈空间
共享程序堆空间
调度由用户控制
协程是轻量级的线程
示意图

goroutine 协程快速入门(示例)
// 在主线程(可以理解成进程)中,开启goroutine,该协程每隔1秒输出"hello,world"
// 在主线程中也每隔一秒输出"hello,golang",输出10次后,退出程序
// 要求主线程和goroutine同时执行
package main
import (
"fmt"
"strconv"
"time"
)
//编写一个函数,每隔1秒输出"hello,world"
func test(){
for i:=1;i<=5;i++{
fmt.Println("test hello,world"+ strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main(){
go test()//开启一个协程,同时与主函数共同执行
for i:=1;i<=5;i++{
fmt.Println("main,Golang"+ strconv.Itoa(i))
time.Sleep(time.Second)
}
}
//执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\goroutinedemo\main.go
// main,Golang1
// test hello,world1
// test hello,world2
// main,Golang2
// test hello,world3
// main,Golang3
// main,Golang4
// test hello,world4
// test hello,world5
// main,Golang5
示意图

小结
1)主线程是一个物理线程,直接作用在CPU上的,是重量级的,非常消耗CPU资源
2)协程从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗较小
3)Glang的协程机制是重要特点,可以轻松开启上万个协程,其他编程语言的并发机制是基于一般线程的,开启过多的线程,资源消耗大,这里就突显Golang在并发上的优势
goroutine 的调度模型
GMP

1)M:操作系统的主线程(物理线程)(Machine(线程))
2)P:协程执行需要的上下文(Processor(调度器/逻辑处理器))
3) G:协程(Goroutine(协程))
GMP 模式运行的状态1

1)当前程序有3个M,如果3个M都运行在一个CPU上,就是并发,如果在不同的CPU运行就是并行
2)M1,M2,M3正在执行一个G(协程),M1有的协程队列有3个,M2有的协程队列有3个,M3有的协程队列有2个
3)从图上看,Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程
4)其他程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光CPU
GMP 模式运行的状态2

1)分成两个部分来看
2)原来的情况是M0主线程正在执行G0协程,另外有三个协程在队列等待
3) 如果G0协程阻塞,例如读取文件或者数据库等
4)这时就会创建M1主线程(也可能从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件IO的读写
5)这样的GPM调度模式,可以既让G0执行,同时也不会让队列其他协程阻塞,仍然可以并发/并行执行
6)等到G0不阻塞了,M0会被放到空闲主线程继续执行(从已有的线程池中取),同时G0又被唤起
深入理解
1️⃣ 创建 Goroutine
go func() {}
👉 创建 G → 放入 P 的本地队列
2️⃣ M 从 P 取任务
👉 M(线程)会从 P 拿一个 G 执行
3️⃣ 执行 Goroutine
👉 在 CPU 上运行
4️⃣ 阻塞时(关键点)
如果 G 阻塞(比如 IO):
👉 M 会去执行别的 G
👉 不会浪费线程
关键机制(面试级重点)
1️⃣ 工作窃取(Work Stealing)
👉 如果某个 P 没任务:
👉 会去别的 P 偷任务
P1: 空 → 去 P2 偷 G
2️⃣ 本地队列 + 全局队列
每个 P 有本地队列(优先)
还有一个全局队列
3️⃣ 抢占式调度(Go 1.14+)
👉 以前是协作式
👉 现在支持“强制切换”
防止:
for {} // 死循环卡死
4️⃣ GOMAXPROCS(非常重要)
runtime.GOMAXPROCS(n)
👉 控制 P 的数量(≈ CPU 核数)
角色解释(必须理解)
1️⃣ G(Goroutine)
👉 你写的:
go func() {
fmt.Println("hello")
}()
👉 就是一个 G
✔ 很轻量(几 KB)
✔ 数量可以非常多(几十万级)
2️⃣ M(线程)
👉 操作系统线程
✔ 真正执行代码的
✔ 数量有限(不能太多)
3️⃣ P(调度器)
👉 Go 运行时的核心
✔ 负责管理 Goroutine 队列
✔ 控制执行顺序
👉 可以理解为:
CPU 的“执行名额”
调度关系(非常重要)
G(协程) → 放在 P 队列 → 由 M 执行 🔥 图解理解 [G][G][G] → P → M(线程) → CPU
设置Glang 运行的CPU数量
介绍: 为了充分利用多CPU的优势,在Golang程序中,设置运行的CPU数目
package main
import(
"fmt"
"runtime"
)
func main(){
//获取当前系统CPU 个数
num:=runtime.NumCPU()
//这里设置num-1的CPU运行Go程序
runtime.GOMAXPROCS(num)
fmt.Println("cpu个数=",num)
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\cpugoroutinedemo02\main.go
// cpu个数= 8
package runtime 介绍
runtime包提供和go运行时环境的互操作,如控制go程的函数。它也包括用于reflect包的低层次类型信息;参见reflect报的文档获取运行时类型系统的可编程接口。 目前常用函数 func NumCPU func NumCPU() int NumCPU返回本地机器的逻辑CPU个数。 func GOMAXPROCS func GOMAXPROCS(n int) int GOMAXPROCS设置可同时执行的最大CPU数,并返回先前的设置。 若 n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过 NumCPU 查询。本函数在调度程序优化后会去掉。
1) go1.8后,默认让程序运行在多个核上,可以不用设置
2)go1.8前,需要设置,可以更高效的利用CPU
channel (管道) 看个人需求
需求: 现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入Map 中,最后显示出来,要求使用goroutine 完成
分析思路:
1)使用goroutine来完成,效率高,但是会出现并发/并行安全问题
2)这里提出了不同goroutine如何通信的问题
代码实现
1)使用goroutine来完成
2)运行某个程序时,如何知道是否存在资源竞争问题,方法,在编译该程序时增加一个参数 -rece即可
报错示例
package main
import (
"fmt"
//"testing"
"time"
)
//思路
//1 编写一个函数,计算各个数的阶乘,并放入map
//2 启动的协程多个统一的将结果放入到map
//map 应该是全局的
var (
myMap=make(map[int]int,10)
)
//test
func tset(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 tset(i)
}
time.Sleep(time.Second*10)
for k,v:=range myMap{
fmt.Printf("%v!%v\n",k,v)
}
}
//执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\goroutinedemo03\main.go
// fatal error: concurrent map writes
// goroutine 21 [running]:
// main.tset(...)
// D:/golang/goproject/src/src01/go_code/src/chapter17/goroutinedemo03/main.go:22
// created by main.main in goroutine 1
示意图 同时并发往map 里写数据引起

解决方案
不同协程之间如何通讯
1)全局变量加锁同步
2)channel
使用全局变量加锁同步改进程序
因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
解决方案加入:互斥锁
数的阶乘很大,结果会越界,可以将求阶乘改成sum+=uint(i)
package sync
import "sync" sync包提供了基本的同步基元,如互斥锁。除了Once和WaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。
type Mutex
type Mutex struct {
// 包含隐藏或非导出字段
}
Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。
func (*Mutex) Lock
func (m *Mutex) Lock()
Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁。
func (*Mutex) Unlock
func (m *Mutex) Unlock()
Unlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。
修改后版本1
package main
import (
"fmt"
//"unicode"s
//"testing"
"time"
"sync"
)
//思路
//1 编写一个函数,计算各个数的阶乘,并放入map
//2 启动的协程多个统一的将结果放入到map
//map 应该是全局的
var (
myMap=make(map[int]uint,10)
//声明全局互斥锁
//sync是包
//mutex互斥意思
lock sync.Mutex
)
//test
func tset(n int){
res :=uint(1)
for i:=1;i<=n;i++{
res *=uint(i)
}
//写操作之前加锁
lock.Lock()
myMap[n]=res //fatal error: concurrent map writes
lock.Unlock() //解锁
}
func main(){
for i :=1 ;i<=10;i++{
go tset(i)
}
time.Sleep(time.Second*10)
for k,v:=range myMap{
fmt.Printf("map[%v]=%v\n",k,v)
}
}
//执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\goroutinedemo03\main.go
// map[1]=1
// map[7]=5040
// map[8]=40320
// map[6]=720
// map[9]=362880
// map[10]=3628800
// map[2]=2
// map[3]=6
// map[4]=24
// map[5]=120
没有休眠时间或者休眠时间过短读的时候也要加锁
package main
import (
"fmt"
//"unicode"s
//"testing"
//"time"
"sync"
)
//思路
//1 编写一个函数,计算各个数的阶乘,并放入map
//2 启动的协程多个统一的将结果放入到map
//map 应该是全局的
var (
myMap=make(map[int]uint,10)
//声明全局互斥锁
//sync是包
//mutex互斥意思
lock sync.Mutex
)
//test
func tset(n int){
res :=uint(1)
for i:=1;i<=n;i++{
res *=uint(i)
}
//写操作之前加锁
lock.Lock()
myMap[n]=res //fatal error: concurrent map writes
lock.Unlock() //解锁
}
func main(){
for i :=1 ;i<=10;i++{
go tset(i)
}
//time.Sleep(time.Second*10)
lock.Lock()
for k,v:=range myMap{
fmt.Printf("map[%v]=%v\n",k,v)
}
lock.Unlock()
}
//执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\goroutinedemo03\main.go
// map[4]=24
// map[6]=720
// map[7]=5040
// map[1]=1
// map[3]=6
// map[2]=2
// map[5]=120
// map[9]=362880
读的部分为什么加锁,按理说休眠时间几秒上面协程也结束了,后面不应该出现资源竞争的问题,但是在运行时候,还是可能出现有资源竞争问题,因为程序从设计上可以指定10秒就执行完成所有协程,但是主线程并不知道,因此底层可能仍然出现资源争夺,因此加入互斥锁
type WaitGroup struct {
// 包含隐藏或非导出字段
}
WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。
func (*WaitGroup) Add
func (wg *WaitGroup) Add(delta int)
Add方法向内部计数加上delta,delta可以是负数;如果内部计数器变为0,Wait方法阻塞等待的所有线程都会释放,如果计数器小于0,方法panic。注意Add加上正数的调用应在Wait之前,否则Wait可能只会等待很少的线程。一般来说本方法应在创建新的线程或者其他应等待的事件之前调用。
func (*WaitGroup) Done
func (wg *WaitGroup) Done()
Done方法减少WaitGroup计数器的值,应在线程的最后执行。
func (*WaitGroup) Wait
func (wg *WaitGroup) Wait()
Wait方法阻塞直到WaitGroup计数器减为0。
使用waitGroup修改2
package main
import (
"fmt"
//"unicode"s
//"testing"
//"time"
"sync"
)
//思路
//1 编写一个函数,计算各个数的阶乘,并放入map
//2 启动的协程多个统一的将结果放入到map
//map 应该是全局的
var (
myMap=make(map[int]uint,10)
//声明全局互斥锁
//sync是包
//mutex互斥意思
lock sync.Mutex
wg sync.WaitGroup
)
//test
func tset(n int){
defer wg.Done()
res :=uint(1)
for i:=1;i<=n;i++{
res *=uint(i)
}
//写操作之前加锁
lock.Lock()
myMap[n]=res //fatal error: concurrent map writes
lock.Unlock() //解锁
}
func main(){
for i :=1 ;i<=10;i++{
wg.Add(1)
go tset(i)
}
wg.Wait()
//time.Sleep(time.Second*10)
for k,v:=range myMap{
fmt.Printf("map[%v]=%v\n",k,v)
}
}
//执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\goroutinedemo03\main.go
// map[1]=1
// map[5]=120
// map[9]=362880
// map[10]=3628800
// map[8]=40320
// map[4]=24
// map[2]=2
// map[3]=6
// map[6]=720
// map[7]=5040
channel管道-基本介绍
为什么需要管道
前面使用全局变量加锁同步来解决goroutine的通讯,并不完美
1)主线程在等待所有goroutine全部完成的时间很难确定,这里设置10秒,仅仅是估算
2)如果主线程休眠时间长了,会加长等待时间,如果短了,可能还没有goruntine处于工作状态,这时也会随主线程的退出而销毁
3)通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
4)上面种种分析都在呼唤一个新的通讯机制-channel
管道介绍
1)channel本质就是一个数据结构队列
2)数据是先进先出【FIFO】
3)线程安全,多个goroutine访问时,不需要枷锁,就是说channel本身就是线程安全的
4)channel是有类型的,一个string的channel只能存放string类型的数据

管道的基本使用
定义/声明
var intChan chan int //intChan 用于存放int数据类型 var mapChan chan map[int]string //(mapChan用于存放map[int]string类型) var perChan chan Person var perChan chan *Persona
说明
1)channel 是引用类型
2)channel必须初始化才能写入数据,即make后才能使用
3)管道是有类型的,intChan 只能写入整数int
package main
import "fmt"
func main(){
var intChan chan int //intChan 用于存放int数据类型
intChan = make(chan int,3)
// var mapChan chan map[int]string //(mapChan用于存放map[int]string类型)
// var perChan chan Person
// var perChan chan *Person
fmt.Printf("intChan的值=%v,intChan本身的地址值=%v\n",intChan,&intChan)
//向管道写入数据
intChan<-10
num := 211
intChan <-num
//当我们给管道写入数据时,不能超过其容量(这个管道容量是3)
intChan<-89
//当超过管道容量时会报错
// fatal error: all goroutines are asleep - deadlock!
// goroutine 1 [chan send]:
//intChan<-67 超出容量无法写入
//看看管道的长度和cap(容量),
fmt.Printf("channel len=%v,cap=%v\n",len(intChan),cap(intChan))
//管道读取数据
var num2 int
num2 =<-intChan//取出第一个数据交给
fmt.Println(num2)
fmt.Printf("channel len=%v,cap=%v\n",len(intChan),cap(intChan))
//在没有使用协程的情况下 管道数据已经全部取出来,再取就会报告deadlock
num3 :=<-intChan
fmt.Println(num3)
fmt.Printf("channel len=%v,cap=%v\n",len(intChan),cap(intChan))
num4 :=<-intChan
fmt.Println(num4)
fmt.Printf("channel len=%v,cap=%v\n",len(intChan),cap(intChan))
//管道数据已经全部取出来,再取就会报告deadlock
num5 :=<-intChan
fmt.Println(num5)
fmt.Printf("channel len=%v,cap=%v\n",len(intChan),cap(intChan))
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo01\main.go
// intChan的值=0xc00001e100,intChan本身的地址值=0xc000058028
// channel len=3,cap=3
// 10
// channel len=2,cap=3
// 211
// channel len=1,cap=3
// 89
// channel len=0,cap=3
// fatal error: all goroutines are asleep - deadlock!
// goroutine 1 [chan receive]:
// main.main()
// D:/golang/goproject/src/src01/go_code/src/chapter17/channeldemo01/main.go:37 +0x593
// exit status 2
注意事项
1)channel中只能存放指定的数据类型
2)channel的数据放满后,就不能再放入了
3)如果从channel取出数据后,可以继续放入
4)在没有使用协程的情况下,如果channel数据取完,再取,就会再报dead lock
读写channel 的演示
创建一个mapChan,最多可以存放10个map[string]string的key-val,演示读取
package main
import "fmt"
func main(){
var mapChan chan map[string]string
mapChan = make(chan map[string]string,10)
m1:=make(map[string]string,10)
m1["city1"]= "北京"
m1["city2"]="天津"
m2:=make(map[string]string)
m2["hero1"]="宋江"
m2["hero2"]="武松"
mapChan <-m1
mapChan <-m2
fmt.Printf("长度=%v,容量=%v\n",len(mapChan),cap(mapChan))
//<-mapChan
fmt.Printf("m1=%v\n",<-mapChan)//取出m1的map直接格式化输出
fmt.Printf("长度=%v,容量=%v\n",len(mapChan),cap(mapChan))
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo02\main.go
// 长度=2,容量=10
// m1=map[city1:北京 city2:天津]
// 长度=1,容量=10
3)创建一个catChannel。最多10个结构体变量
package main
import "fmt"
type cat struct{
Name string
Age int
}
func main(){
var catChan chan cat
catChan = make(chan cat,10)
cat1:=cat{"小鹿",56}
cat2:=cat{"小狗",16}
catChan <-cat1 //写入
catChan<- cat2 //写入
fmt.Printf("长度=%v,容量=%v\n",len(catChan),cap(catChan))
fmt.Printf("cat1=%v\n",<-catChan)
fmt.Printf("长度=%v,容量=%v\n",len(catChan),cap(catChan))
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo02\main.go
// 长度=2,容量=10
// cat1={小鹿 56}
// 长度=1,容量=10
创建catChan2.最多可以存放10个*cat变量,演示写入和读取
package main
import "fmt"
type cat struct{
Name string
Age int
}
func main(){
var catChan chan *cat
catChan = make(chan *cat,10)
cat1 := cat{"lu",16}
cat2 := cat{"t1",17}
catChan<-&cat1
catChan<-&cat2
cat11:=<-catChan
fmt.Printf("长度=%v,容量=%v\n",len(catChan),cap(catChan))
fmt.Println(*cat11)
fmt.Println(*(<-catChan))
fmt.Printf("长度=%v,容量=%v\n",len(catChan),cap(catChan))
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo02\main.go
// 长度=1,容量=10
// {lu 16}
// {t1 17}
// 长度=0,容量=10
5)创建一个allChan。最多放10个任意类型数据
package main
import "fmt"
type cat struct{
Name string
Age int
}
func main(){
var allChan chan interface{}
allChan= make(chan interface{},10)
allChan<-cat1
allChan<-&cat1
allChan<-"晨曦"
allChan<-78.6
allChan<-89
fmt.Printf("长度=%v,容量=%v\n",len(allChan),cap(allChan))
a := (<-allChan).(cat)//这里类型断言
fmt.Println(a.Name)//已经类型断言转换成cat结构所有可以直接调Name字段
fmt.Println((<-allChan).(*cat).Name) //这里取Name值需要类型断言
fmt.Println(<-allChan)
fmt.Printf("长度=%v,容量=%v\n",len(allChan),cap(allChan))
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo02\main.go
// 长度=5,容量=10
// lu
// lu
// 晨曦
// 长度=2,容量=10
示例2断言1
package main
import "fmt"
type cat struct{
Name string
Age int
}
func main(){
// var mapChan chan map[string]string
// mapChan = make(chan map[string]string,10)
// m1:=make(map[string]string,10)
// m1["city1"]= "北京"
// m1["city2"]="天津"
// m2:=make(map[string]string)
// m2["hero1"]="宋江"
// m2["hero2"]="武松"
// mapChan <-m1
// mapChan <-m2
// fmt.Printf("长度=%v,容量=%v\n",len(mapChan),cap(mapChan))
// //<-mapChan
// fmt.Printf("m1=%v\n",<-mapChan)//取出m1的map直接格式化输出
// fmt.Printf("长度=%v,容量=%v\n",len(mapChan),cap(mapChan))
// var catChan chan cat
// catChan = make(chan cat,10)
// cat1:=cat{"小鹿",56}
// cat2:=cat{"小狗",16}
// catChan <-cat1 //写入
// catChan<- cat2 //写入
// fmt.Printf("长度=%v,容量=%v\n",len(catChan),cap(catChan))
// fmt.Printf("cat1=%v\n",<-catChan)
// fmt.Printf("长度=%v,容量=%v\n",len(catChan),cap(catChan))
// var catChan chan *cat
// catChan = make(chan *cat,10)
cat1 := cat{"lu",16}
// cat2 := cat{"t1",17}
// catChan<-&cat1
// catChan<-&cat2
// cat11:=<-catChan
// fmt.Printf("长度=%v,容量=%v\n",len(catChan),cap(catChan))
// fmt.Println(*cat11)
// fmt.Println(*(<-catChan))
// fmt.Printf("长度=%v,容量=%v\n",len(catChan),cap(catChan))
var allChan chan interface{}
allChan= make(chan interface{},10)
allChan<-cat1
allChan<-&cat1
allChan<-"晨曦"
allChan<-78.6
allChan<-89
fmt.Printf("长度=%v,容量=%v\n",len(allChan),cap(allChan))
a := (<-allChan).(cat)//这里类型断言
fmt.Println(a.Name)//已经类型断言转换成cat结构所有可以直接调Name字段
fmt.Println((<-allChan).(*cat).Name) //这里取Name值需要类型断言
fmt.Println(<-allChan)
fmt.Printf("长度=%v,容量=%v\n",len(allChan),cap(allChan))
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo02\main.go
// 长度=5,容量=10
// lu
// lu
// 晨曦
// 长度=2,容量=10
断言示例2
package main
import "fmt"
type Cat struct{
Name string
Age int
}
func main(){
allChan := make(chan interface{},3)
allChan<-1
allChan<-3
cat := Cat{"小花猫",4}
allChan<-cat
<-allChan
<-allChan
t :=(<-allChan).(Cat)//断言注意类型不能跟变量同名。不然报错
fmt.Println(t.Name)
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo02\main.go
// 小花猫
channel 的关闭与遍历
channel 的关闭
使用内置函数close 可以关闭channel,当channel关闭后,就不能再往里面写数据了,但是仍然可以读
func close
func close(c chan<- Type) 内建函数close关闭信道,该通道必须为双向的或只发送的。它应当只由发送者执行,而不应由接收者执行,其效果是在最后发送的值被接收后停止该通道。在最后的值从已关闭的信道中被接收后,任何对其的接收操作都会无阻塞的成功。对于已关闭的信道,语句: x, ok := <-c 还会将ok置为false。
示例
package main
import (
"fmt"
// "math/rand"
// "time"
)
// type Cat struct{
// Name string
// Age int
// Address string
// }
func main(){
intChan := make(chan int,3)
intChan <- 100
intChan <- 200
close(intChan) //关闭管道;不能写入操作
//intChan <- 300
/*写入操作报错
panic: send on closed channel
goroutine 1 [running]:
main.main()
D:/golang/goproject/src/src01/go_code/src/chapter17/channeldemo06/main.go:18 +0x65
exit status 2*/
//fmt.Println(intChan)
fmt.Println(<-intChan)//还是可以正常读的
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo06\main.go
// 100
channel的遍历
channel 支持for-range的方式进行遍历,请注意两细节
1)在遍历时,如果channel 没有关闭,则会出现deadlock的错误
2)在遍历时,如果channel已经关闭,则会正常遍历。遍历完成后,就会退出
遍历管道一定要close(intChan)
package main
import (
"fmt"
)
func main(){
intChan := make(chan int,3)
intChan <- 100
intChan <- 200
close(intChan) //关闭管道;不能写入操作
//intChan <- 300
/*写入操作报错
panic: send on closed channel
goroutine 1 [running]:
main.main()
D:/golang/goproject/src/src01/go_code/src/chapter17/channeldemo06/main.go:18 +0x65
exit status 2*/
//fmt.Println(intChan)
fmt.Println(<-intChan)//还是可以正常读的
intChan2 := make(chan int,100)
for i:=0;i<100;i++{
intChan2 <- i*2
}
close(intChan2)//如果没有这一行关闭管道操作则报all goroutines are asleep - deadlock!
//遍历管道时只能使用for-range
for v:=range intChan2 {
fmt.Println("v",v)
}
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo06\main.go
// v 0
// ...
// v 198
协程配合管道的应用
完成goroutine 和channel 协同工作工作示例
1)开启一个writeData协程,向管道intChan写入50个整数
2)开启一个reaData协程,从管道intChan中读取writeData写入的数据
3)注意: writeData协程和reaData协程操作的是同一个管道
4)主线程需要等待writeData 和reaData 协程完成才可以退出
代码实现
package main
import (
"fmt"
"time"
)
//write
func writeData(intchan chan int){
for i :=1; i<51;i++{
intchan <-i
fmt.Println("writeData",i)
time.Sleep(time.Second)//休眠1秒看交互效果
}
close(intchan)//关闭停止写
}
func reaData(intchan chan int,execchan chan bool){
for{
v ,ok :=<-intchan
if !ok {
break
}
fmt.Printf("readData 读到数据了=%v\n",v)
}
execchan <-true
}
func main(){
//创建
intChan := make(chan int,50)
exitChan := make(chan bool,1)
go writeData(intChan) //协程
go reaData(intChan,exitChan) //协程
for {
t :=<-exitChan
if t {
break
}
}
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo07\main.go
// writeData 1
// readData 读到数据了=1
// writeData 2
// readData 读到数据了=2
// writeData 3
// ...
// readData 读到数据了=50
示意图

阻塞
如果只向管道只管写没有读取,就会出现阻塞dead lock,原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在witeData的ch<-i
package main
import (
"fmt"
"time"
)
//write
func writeData(intchan chan int){
for i :=1; i<=50;i++{
intchan <-i
fmt.Println("writeData",i)
//time.Sleep(time.Second)//休眠1秒看交互效果
}
close(intchan)//关闭停止写
}
func reaData(intchan chan int,execchan chan bool){
for{
time.Sleep(time.Second)
_,ok :=<-intchan
if !ok {
break
}
fmt.Println("reaData",<-intchan)
}
execchan <-true
}
func main(){
//创建
intChan := make(chan int,10)
//reaChan := make(chan int,10)
exitChan := make(chan bool,1)
go writeData(intChan) //协程
go reaData(intChan,exitChan) //协程
for {
_,t :=<-exitChan
if t {
break
}
}
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo07\main.go
// writeData 1
// writeData 2
// writeData 3
// writeData 4
// writeData 5
// writeData 6
// writeData 7
// writeData 8
// writeData 9
// writeData 10
// ...
// reaData 50
小结:如果编译器(运行),发现一个管道只有写,而没有读,会阻塞,而写管道和读管道速率不一致,无所谓
统计1-8000数字中,哪些是素数用协程和管道
思路分析

方式1
package main
import (
"fmt"
)
func primeNum(ints chan int,prim chan int,exits chan bool){
//使用
//var num int
var flag bool
for {
num,ok :=<-ints
if !ok{
break
}
flag = true
//判断是不是素数
for i:=2;i<num;i++{
// if num ==2 || num ==1{
// prim <-num
// }else if num % i==0{
// }
if num % i == 0{//说明不是素数
flag=false
break
}
}
if flag {
prim <-num
}
}
fmt.Println("有一个协程完成工作")
exits<-true
}
func main(){
intChan:=make(chan int,1000)
primeChan := make(chan int,2000)//
//标识退出管道
exit := make(chan bool,4)
//开启协程响,放intChan放入1-8000个数
go func (intChan chan int){ //通过匿名函数
for i :=0; i<80;i++{
intChan<-i
}
close(intChan)
}(intChan)
//开启4个协程从int 管道取数据,并判断是不是为素数,如果是放入到primechan
for i :=0 ;i <4 ;i++{
go primeNum(intChan,primeChan,exit)
}
go func () {
for i:=0;i<4;i++{
<-exit
}
close(primeChan)
}()
for {
res,ok :=<-primeChan
if !ok {
break
}
fmt.Printf("素数=%d\n",res)
}
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo09\main.go
// 有一个协程完成工作
// 有一个协程完成工作
// 有一个协程完成工作
// 有一个协程完成工作
// 素数=0
// 素数=1
// 素数=2
// 素数=3
// ...
// 素数=79
方式2 通过int变量实现退出
package main
import (
"fmt"
)
func primeNum(ints chan int,prim chan int,x *int){
//使用
//var num int
var flag bool
for {
num,ok :=<-ints
if !ok{
break
}
flag = true
//判断是不是素数
for i:=2;i<num;i++{
if num % i == 0{//说明不是素数
flag=false
break
}
}
if flag {
prim <-num
}
}
fmt.Println("有一个协程完成工作")
*x++
}
func main(){
intChan:=make(chan int,1000)
primeChan := make(chan int,2000)//
go func (intChan chan int){ //匿名函数
for i :=0; i<80;i++{
intChan<-i
}
close(intChan)
}(intChan)
//开启4个协程从int 管道取数据,并判断是不是为素数,如果是放入到primechan
var x int
for i :=0 ;i <4 ;i++{
go primeNum(intChan,primeChan,&x)
}
for {
if x == 4{
close(primeChan)
break
}
}
for {
res,ok :=<-primeChan
if !ok {
break
}
fmt.Printf("素数=%d\n",res)
}
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo09\main.go
// 有一个协程完成工作
// 有一个协程完成工作
// 有一个协程完成工作
// 有一个协程完成工作
// 素数=0
// 素数=1
// ...
// 素数=73
// 素数=79
示例
package main
import (
"fmt"
//"time"
)
func main(){
intchan := make(chan int,200)
numchan := make(chan int,200)
b := make(chan bool,8)
go func (intchan chan int){
for i :=0; i<200;i++{
intchan <- i
}
close(intchan)
}(intchan)
r := func (intchan chan int,numchan chan int,b chan bool) {
for {
c := 0
v,ok := <-intchan
if !ok{
b <- true
break
}
for i:=1;i <=v;i++{
c+=i
}
numchan <-c
}
}
for i :=0 ;i<8;i++{
go r(intchan,numchan,b)
}
// for {
// // if t ==8 {
// // close(numchan)
// // break
// // }
// }
for i :=0 ;i<8;i++{
<-b
}
close(numchan)
for {
v,ok := <-numchan
if !ok{
break
}
fmt.Printf("v=%v\n",v)
}
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo11\main.go
// v=0
// v=1
// v=3
// ...
// v=19306
1) channel 可以声明只读,或只写
package main
import (
"fmt"
//"time"
)
func main(){
//管道可以声明只读或者只写
//1.在默认情况下,管道是双向的
// var chan1 chan int //可读可写
//2声明为只写
var chan1 chan<- int
chan1 = make(chan int,3)
chan1 <-1
//chan1<-2
//chan1<-3
//fmt.Println(<-chan1)//invalid operation: cannot receive from send-only channel chan1 (variable of type chan<- int)
/*报错提示不可以读
invalid operation: cannot receive from send-only channel chan1 (variable of type chan<- int)
*/
var chan2 <-chan int
chan2 <- 67 /* 报错提示不可以写
invalid operation: cannot send to receive-only channel chan2 (variable of type <-chan int)
*/
num :=<- chan2
fmt.Println("num",num)
}
2)读写实践

3)使用select 可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
//"time"
)
func main(){
// 使用select 可以解决从管道取数据的阻塞问题
// 定义一个管道10个数据
intchan := make(chan int,10)
for i :=0;i <10; i ++{
intchan <- i
}
strchan:=make(chan string,5)
for i := 0;i<5;i++{
strchan <- "helloo"+fmt.Sprintf("%v",i)
}
//传统的方法遍历管道时不关闭会阻塞,导致死锁
//问题在实际开发中不好确定什么时候开关管道;可以使用selectl 方式也可以解决
t1:
for {
select {
case v:=<-intchan://注意:这里,如果intchan一直没有关闭,不会一直阻塞而导致死锁;会自动到下一个case匹配
fmt.Println("从intchan读取了数据",v)
case v:= <-strchan:
fmt.Println("从strchan读取了数据",v)
default:
fmt.Println("都取不到数据,加入自己逻辑")
break t1
}
}
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo13\main.go
// 从intchan读取了数据 0
// 从intchan读取了数据 1
// 从strchan读取了数据 helloo0
// 从intchan读取了数据 2
// 从intchan读取了数据 3
// 从strchan读取了数据 helloo1
// 从strchan读取了数据 helloo2
// 从strchan读取了数据 helloo3
// 从intchan读取了数据 4
// 从strchan读取了数据 helloo4
// 从intchan读取了数据 5
// 从intchan读取了数据 6
// 从intchan读取了数据 7
// 从intchan读取了数据 8
// 从intchan读取了数据 9
// 都取不到数据,加入自己逻辑
4)gorountine 中使用recover,解决协程中出现panic。导致程序崩溃
说明:如果启动一个协程,但是协程出现了panic,如果没有捕获这个panic,就会造成正常程序崩溃,这时可以在goroutine中使用recover 来捕获panic,进行处理,这样即使这个协程发生的问题,也不影响主线程,主线程可以继续执行
package main
import (
"fmt"
"time"
//"time"
)
//函数
func sayHello(){
for i :=0; i <10;i ++{
time.Sleep(time.Second)
fmt.Println("hello")
}
}
func test(){
// 这里使用错误处理机制defer +recover
defer func () {
//捕获抛出的异常
if err:= recover();err !=nil{
fmt.Println("test()协程发生异常了err=",err)
}
}()
/* 如果没有这个defer ,就会直接中断程序
PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo14\main.go
panic: assignment to entry in nil map
goroutine 7 [running]:
main.test()
D:/golang/goproject/src/src01/go_code/src/chapter17/channeldemo14/main.go:20 +0x25
created by main.main in goroutine 1
D:/golang/goproject/src/src01/go_code/src/chapter17/channeldemo14/main.go:25 +0x2a
exit status 2
PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo14\main.go
# command-line-arguments
*/
//t := make(map[string]string)
var t map[int]string
t[1]= "golang"
}
func main(){
go sayHello()
go test()
for i:=0;i <10 ;i++{
time.Sleep(time.Second)
fmt.Println("hello main")
}
}
// 执行结果
// PS D:\golang\goproject\src\src01\go_code\src> go run chapter17\channeldemo14\main.go
// test()协程发生异常了err= assignment to entry in nil map
// hello main
// hello
// hello
// hello main
// hello main
// hello
// hello
// hello main
// hello main
// hello
// hello
// hello main
// hello main
// hello
// hello main
// hello
// hello
// hello main
// hello main

浙公网安备 33010602011771号