Chapter016 goroutine协程 和 channel管道
上一章:Chapter014 golang单元测试本章纲要一、进程和线程二、并发和并行(go程序的)三、go的协程和go的主线程四、快速入门五、goroutine的调度模型 MPG一、进程和线程1、百度百科进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执...
本章纲要
一、进程和线程
1、百度百科
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
2、举个例子:
- 打开一个迅雷应用,那么迅雷应用就有一个进程,如果选择下载多个文件,那么很多个下载任务就是一个个的线程。(有些程序还可以多进程)
二、并发和并行(go程序的)
1、简述
- 多线程程序在单核
- 多线程程序在多核
2、并发的特点
- 多个任务作用在一个cpu上
- 从微观的角度来看,在一个时间点上,其实只有一个任务在执行
3、并行的特点
- 多个任务作用在多个cpu上
- 从微观的角度来看,在一个时间点上,有多个任务在同时执行
三、go的协程和go的主线程
1、简单说明
- Go主线程(有程序员直接称为线程/也可以理解为进程)
- 一个Go线程上可以起多个协程,即可以理解为:协程是轻量级的线程
2、GO协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
四、gorountine 快速入门
1、编写一个程序,完成如下功能:
- 在主线程中(在进程中),开启一个gorountine ,该协程每隔1s输出一个“hello world”
- 在主线程中也每隔一秒输出“hello,golang”,输出10次后,退出程序
- 要求主线程和gorountine同时执行
- 要求主线程和协程执行流程图
- 注意事项:
1))如果主线程退出了,则协程即使还没有执行完毕,也会退出
2))当然协程也可以在主线程没有结束之前就自己结束了,比如完成了自己的任务 - 小结
1))主线程是一个物理线程直接作用在CPU上,是重量级的,非常消耗CPU资源
2))协程是从主线程开启的,是轻量级的线程,是逻辑态的,对资源消耗相对较小
package main
import (
"fmt"
_ "fmt"
"strconv"
"time"
)
//1) 在主线程中(在进程中),开启一个gorountine ,该协程每隔1s输出一个“hello world”
//2) 在主线程中也每隔一秒输出“hello,golang”,输出10次后,退出程序
//3) 要求主线程和gorountine同时执行
//4) 要求主线程和协程执行流程图
func test01() {
for i:= 1; i<=4;i++{
fmt.Println("test01(): hello world",strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func test02() {
for i:= 1; i<=4;i++{
fmt.Println("test02(): hello world",strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
test01()
go test02() //开启协程 ,输出test02穿插
for i:= 1; i<=4;i++{
fmt.Println("main(): hello golang",strconv.Itoa(i))
time.Sleep(time.Second)
}
}
运行结果:
五、goroutine的调度模型 MPG
1、简单介绍
- M:操作系统的主线程(是物理线程)
- P:协程执行需要的上下文
- G:协程
2、设置Golang运行的cpu数


六、channel 管道引例
1、main.go
package main
import (
"fmt"
)
//全局变量myMap
var (
myMap = make(map[int]int,10)
)
//计算n的阶乘保存到myMap
func test(n int) {
res :=1
for i :=1;i<=n;i++{
res *=i
}
myMap[n] = res
}
func main() {
//开启协程完成任务
for i:=1;i<=200;i++{
go test(i)
}
//输出结果
for i,v := range myMap{
fmt.Printf("第【%v】= %v\n",i,v)
}
}
代码这样写看似没有问题,但是没有输出结果或者不全,原因是主线程完成任务后退出程序,然而协程还没有完成
2、main.go
package main
import (
"fmt"
)
//全局变量myMap
var (
myMap = make(map[int]int,10)
)
//计算n的阶乘保存到myMap
func test(n int) {
res :=1
for i :=1;i<=n;i++{
res *=i
}
myMap[n] = res
}
func main() {
//开启协程完成任务
for i:=1;i<=200;i++{
go test(i)
}
//*******************************************************添加
time.Sleep(10*time.Second)
//输出结果
for i,v := range myMap{
fmt.Printf("第【%v】= %v\n",i,v)
}
}
如果添加*标明的time.Sleep(),会报错没有保护机制,原因很简单,有200个协程同时操作同一块地址

tips:在运行时,加上参数-race可以知道是否存在资源竞争
go build -race main.go

3、问题解决方法:
(1)全局变量加锁同步
加入互斥锁:
main.go
package main
import (
"fmt"
"sync"
"time"
)
//全局变量myMap
var (
myMap = make(map[int]int,10)
//lock为全局互斥锁
//sync 为一个包 同步
//Mutex 结构体 互斥
lock sync.Mutex
)
//计算n的阶乘保存到myMap
func test(n int) {
res :=1
for i :=1;i<=n;i++{
res *=i
}
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
}
func main() {
//开启协程完成任务
for i:=1;i<=200;i++{
go test(i)
}
time.Sleep(time.Second*10)
//输出结果
//加锁
lock.Lock()
for i,v := range myMap{
fmt.Printf("第【%v】= %v\n",i,v)
}
//解锁
lock.Unlock()
}
运行结果:
为什么还要进行如下加锁呢?
原因很简单,因为我们从程序设计上知道10s可以完成所有协程,单数主线程不知道,因此底层仍然可能出现资源争夺,因此还是要加上互斥锁解决问题
(2)channel管道
七、channel 的基本介绍
1、channel本质是一个数据结构——队列
2、先进先出
3、线程安全、多goroutine访问时,不需要加锁,就是说channel本质是线程安全的
4、channel是有类型的,一个string的channel只能存放string类型数据
八、channel 的使用
1、声明定义
(1)管道存放基本数据类型
var 变量名 chan 数据类型
(2)管道存放map
var mapChan chan map[int]string (mapChan用于存放map[int]string类型)
(3)管道存放结构体
var perChan chan Person
(4)管道存放结构体指针
var perChan2 chan *Person
2、使用说明
(1)chan是引用类型
package main
import "fmt"
func main() {
var intChan chan int
intChan = make(chan int,3)
fmt.Printf("%v",intChan)
}

(2)chan必须初始化才能写入数据,即make后使用
(3)管道是有类型的
3、初始化以及使用
package main
import "fmt"
func main() {
var intChan chan int
intChan = make(chan int,3)
fmt.Printf("%v\n",intChan)
//写入数据,最多3个,不能超过其容量
intChan<- 10
num := 211
intChan<- num
//查看管道长度和容量
fmt.Printf("len:%v cap:%v",len(intChan),cap(intChan))
//从管道中读取数据,长度变少,容量不变
//在没有使用协程的情况下,如果我们一直取数据,再取就会报告deadlock
var num2 int
num2 = <-intChan
fmt.Print(num2)
}
tips:
- 从管道中读取数据,长度变少,容量不变
- 在没有使用协程的情况下,如果我们一直取数据,再取就会报告deadlock
- 管道精髓在于边取边放
- 扔掉管道内数据(假设管道名字叫做 intChan )
<-intChan
4、channel的关闭
当管道关闭后没有办法写入,但是可以读
错误maingo
package main
import "fmt"
func main() {
intChan := make(chan int,3)
intChan <- 100
intChan <- 200
close(intChan)
intChan <-300
fmt.Println("ok~")
}

正确main.go
package main
import "fmt"
func main() {
intChan := make(chan int,3)
intChan <- 100
intChan <- 200
close(intChan)
fmt.Println("ok~")
}

5、channel的遍历
(1)支持for-range的遍历
- 在遍历时,如果channel没有关闭,则会出现deadlock的现象
- 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出程序
attention:关闭管道!!!
package main
import "fmt"
func main() {
//遍历管道
intChan2 :=make(chan int,100)
for i:= 0;i<100;i++{
intChan2<- i*2
}
//不关闭管道会发生死锁
close(intChan2)
for v := range intChan2{
fmt.Printf("%v\n",v)
}
}

6、综合案例1
(1)描述:
一边写入管道,一边读管道数据(由于太快,所以可以适当延时)
(2)流程图
package main
import (
"fmt"
"time"
)
func WriteData(intChan chan int) {
for i:=1;i<=10;i++{
intChan<-i
fmt.Println("写入数据",i)
time.Sleep(time.Second)
}
close(intChan)
}
func ReadData(intChan chan int, exitChan chan bool) {
for{
v,ok := <-intChan
if !ok{
break
}
fmt.Printf("readData 读到数据%v\n",v)
time.Sleep(time.Second)
}
exitChan <- true
close(exitChan)
}
func main() {
intChan :=make(chan int,10)
exitChan :=make(chan bool,10)
go WriteData(intChan)
go ReadData(intChan,exitChan)
for {
_,ok := <- exitChan
if !ok{
break
}
}
}

九、gorountine和channel结合
1、案例,输出8000内所有素数
package main
import "fmt"
func putNum(intChan chan int) {
for i:=1;i<=8000;i++{
intChan<-i
}
close(intChan)
}
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool) {
var flag bool
for{
num,ok :=<-intChan
if !ok{
break
}
flag=true
for i:=2;i<num;i++{
if num % i == 0{
flag = false
break
}
}
if flag{
primeChan<-num
}
}
fmt.Println("有一个协程primChan取不到数据,退出")
exitChan<-true
}
func main() {
intChan := make(chan int,1000)
primeChan :=make(chan int,2000)
exitChan :=make(chan bool,4)
//开启一个协程,向intChan 放入数据1-8000个数
go putNum(intChan)
//开启4个协程,从intChan 取出数据,并判断是否为素数
//如果是,就放到primeChan
for i:=0;i<4 ;i++ {
go primeNum(intChan,primeChan,exitChan)
}
//主线程,进行处理
//直接
go func() {
for i:=0;i<4 ;i++ {
<-exitChan
}
//取出4个就可以放心关闭
close(primeChan)
}()
for{
res,ok :=<-primeChan
if !ok{
break
}
fmt.Printf("primeNum=%v\n",res)
}
fmt.Println("main主线程退出")
}

十、管道只读或者只写
1、只写
var chan2 chan<- int
2、只读
var chan3 <-chan int
十一、select解决管道阻塞
1、问题:不关管道容易阻塞
2、解决:
for{
select{
case v1:= <- intChan :
fmt.Printf("从intChan读取的数据为%d\n",v)
case v2:= <- stringChan:
fmt.Printf("从stringChan读取的数据为%s\n",v)
default:
fmt.Printf("都取不到了,可以加上自己逻辑\n",v)
break //return
}
}
十二、gorountine的panic捕获——panic
func test(){
defer func(){ //defer + 匿名函数
//捕获发生的错误
if err:=recover();err !=nil{
fmt.Println("test()发生错误",err)
}
}()
var myMap map[int]string
myMap[0] = "golang"
}
更多推荐


所有评论(0)