Golang 语言之goroutine(协程)channel(管道)

协程引入

需求: 要求统计1-20000的数字中,哪些是素数

分析思路

1)传统的方法,就是使用一个循环,循环的判断各个数是不是素数

2)使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成,这时使用goroutine

基本介绍

进程和线程说明

1)进程就是程序在操作系统中的一次执行过程,使系统进行资源分配和调度的基本单位

2)线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位

3)一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行

4)一个程序至少有一个进程,一个进程知识有一个线程

image

并发和并行

1)多线程程序在单核上运行,就是并发

2)多线程程序在多核上运行,就是并行

示例图

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

image

 

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

image

goroutine 基本介绍

Go 协程和Go 主线程

1)Go主线程(有程序员直接程为线程/也可以理解进程):一个Go线程上,可以起多个协程,可以这样理解,协程是轻量级的线程【编译器做的优化】

2)Go协程的特点

有独立的栈空间

共享程序堆空间

调度由用户控制

协程是轻量级的线程

示意图

image

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

  示意图

image

小结

1)主线程是一个物理线程,直接作用在CPU上的,是重量级的,非常消耗CPU资源

2)协程从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗较小

3)Glang的协程机制是重要特点,可以轻松开启上万个协程,其他编程语言的并发机制是基于一般线程的,开启过多的线程,资源消耗大,这里就突显Golang在并发上的优势

goroutine 的调度模型

GMP

image

1)M:操作系统的主线程(物理线程)(Machine(线程))

2)P:协程执行需要的上下文(Processor(调度器/逻辑处理器))

3) G:协程(Goroutine(协程))

GMP 模式运行的状态1

image

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

image

 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 里写数据引起

image

解决方案

不同协程之间如何通讯

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 

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类型的数据

image

管道的基本使用

定义/声明

	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

  示意图

image

阻塞

如果只向管道只管写没有读取,就会出现阻塞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数字中,哪些是素数用协程和管道

思路分析

image

 

 方式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)读写实践

image

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

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2026-03-27 14:49  烟雨楼台,行云流水  阅读(1)  评论(0)    收藏  举报