扫码订阅《 Go语言核心编程教程》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • 尚硅谷Go语言核心编程教程

    • 课程说明
    • 第1章 GOLANG 开山篇
    • 第2章 GOLANG 的概述
    • 第3章 GOLANG 变量
    • 第4章 运算符
    • 第5章 程序流程控制
    • 第6章 函数、包和错误处理
    • 第7章 数组与切片
    • 第8章 排序和查找
    • 第9章 map
    • 第10章 面向对象编程 ( 上 )
    • 第11章 面向对象编程 ( 下 )
    • 第12章 项目1:家庭收支记账软件项目
    • 第13章 项目2:客户信息关系系统
    • 第14章 文件操作
    • 第15章 单元测试
    • 第16章 goroutine和channel
    • 第17章 反射
    • 第18章 TCP 编程
    • 第19章 REDIS 的使用
    • 第20章 数据结构

扫码订阅《 Go语言核心编程教程》或入驻星球,即可阅读文章!

第16章 goroutine和channel


GOLANG ROADMAP

# 16.1 goroutine-看一个需求

【点击观看视频】goroutine的引出
  • 需求:要求统计 1 - 9000000000 的数字中,哪些是素数?

  • 分析思路:

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

2 ) 使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成,这时就会使用到goroutine.【速度提高 4 倍】

# 16.2 goroutine-基本介绍

【点击观看视频】goroutine的基本介绍

# 16.2.1 进程和线程介绍

  • 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
  • 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
  • 一个进程可以创建销毁多个线程,同一个进程中的多个线程可以并发执行
  • 一个程序至少一个进程,一个进程至少一个线程

# 16.2.2 程序、进程和线程的关系示意图

image-20210119100919321

# 16.2.3 并发和并行

  • 并发和并行

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

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

3 ) 示意图:

image-20210119101020033

小结

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

并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行。

# 16.2.4 Go协程和Go主线程

【点击观看视频】go协程和go主线程
  • Go主线程(有程序员直接称为线程/也可以理解成进程): 一个Go线程上,可以起多个协程,你可以 这样理解,协程是轻量级的线程[编译器做优化]。

  • Go协程的特点

    1 ) 有独立的栈空间

    2 ) 共享程序堆空间

    3 ) 调度由用户控制

    4 ) 协程是轻量级的线程

  • 一个示意图

image-20210119101656168

# 16.3 goroutine-快速入门

【点击观看视频】协程快速入门

# 16.3.1 案例说明

  • 请编写一个程序,完成如下功能:

1 ) 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔 1 秒输出 "hello,world"

2 ) 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序

3 ) 要求主线程和goroutine同时执行

4 ) 画出主线程和协程执行流程图

  • 代码实现
package main
import (
	"fmt"
	"strconv"
	"time"
)

// 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world"
// 在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
// 要求主线程和goroutine同时执行

//编写一个函数,每隔1秒输出 "hello,world"
func test() {
	for i := 1; i <= 10; i++ {
		fmt.Println("tesst () hello,world " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

func main() {

	go test() // 开启了一个协程

	for i := 1; i <= 10; i++ {
		fmt.Println(" main() hello,golang" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

输出的效果说明,main这个主线程和 test 协程同时执行

 main() hello,golang1
tesst () hello,world 1
 main() hello,golang2
tesst () hello,world 2
 main() hello,golang3
tesst () hello,world 3
 main() hello,golang4
tesst () hello,world 4
 main() hello,golang5
tesst () hello,world 5
 main() hello,golang6
tesst () hello,world 6
 main() hello,golang7
tesst () hello,world 7
 main() hello,golang8
tesst () hello,world 8
 main() hello,golang9
tesst () hello,world 9
 main() hello,golang10
tesst () hello,world 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 主线程和协程执行流程图

image-20210119102345080

# 16.3.2 快速入门小结

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

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

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

# 16.4 goroutine的调度模型

【点击观看视频】MPG模式的介绍

# 16.4.1 MPG模式基本介绍

image-20210119102514584

  • M:操作系统的主线程(是物理线程)
  • P:协程执行需要的上下文
  • G:协程

# 16.4.2 MPG模式运行的状态 1

image-20210119103115822

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

# 16.4.3 MPG模式运行的状态 2

image-20210119103637235

# 16.5 设置GOLANG运行的CPU数

【点击观看视频】go设置运行cpu数目
  • 介绍:为了充分了利用多cpu的优势,在Golang程序中,设置运行的cpu数目
package main
import (
	"runtime"
	"fmt"
)

func main() {
	cpuNum := runtime.NumCPU()
	fmt.Println("cpuNum=", cpuNum)

	//可以自己设置使用多个cpu
	runtime.GOMAXPROCS(cpuNum - 1)
	fmt.Println("ok")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • go1.8后,默认让程序运行在多个核上,可以不用设置了
  • go1.8前,还是要设置以下,可以更高效的利用cpu

# 16.6 channel(管道)-看个需求

【点击观看视频】协程并发(并行)资源竞争问题

需求:现在要计算 1 - 200 的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成。

分析思路:

  1. 使用goroutine 来完成,效率高,但是会出现并发/并行安全问题
  2. 这里就提出了不同goroutine如何通信的问题

代码实现

1 ) 使用goroutine来完成(看看使用gorotine并发完成会出现什么问题? 然后我们会去解决)

2 ) 在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单,在编译该程序时,增加一 个参数 -race即可 [示意图]

3 ) 代码实现:

package main
import (
	"fmt"
	"time"
)

// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成 

// 思路
// 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)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

4 ) 示意图:

image-20210119105056555

# 16.6.1 不同goroutine之间如何通讯

1 ) 全局变量的互斥锁

2 ) 使用管道channel来解决

# 16.6.2 使用全局变量加锁同步改进程序

【点击观看视频】全局互斥锁解决资源竞争
  • 因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes
  • 解决方案:加入互斥锁
  • 我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum+=uint 64 (i)
  • 代码改进
package main
import (
	"fmt"
	_ "time"
	"sync"
)

// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成 

// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.

var (
	myMap = make(map[int]int, 10)  
	//声明一个全局的互斥锁
	//lock 是一个全局的互斥锁, 
	//sync 是包: synchornized 同步
	//Mutex : 是互斥
	lock sync.Mutex
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
	
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}

	//这里我们将 res 放入到myMap
	//加锁
	lock.Lock()
	myMap[n] = res //concurrent map writes?
	//解锁
	lock.Unlock()
}

func main() {

	// 我们这里开启多个协程完成这个任务[200个]
	for i := 1; i <= 20; i++ {
		go test(i)
	}

	//休眠10秒钟【第二个问题 】
	//time.Sleep(time.Second * 5)

	//这里我们输出结果,变量这个结果
	lock.Lock()
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
	lock.Unlock()

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 16.6.3 为什么需要channel

  1. 前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
  2. 主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5. 上面种种分析都在呼唤一个新的通讯机制-channel

# 16.6.4 channel的基本介绍

【点击观看视频】管道基本介绍
  1. channle本质就是一个数据结构-队列【示意图】
  2. 数据是先进先出【FIFO:firstinfirstout】
  3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
  4. channel有类型的,一个string的channel只能存放string类型数据。
  5. 示意图:

image-20210119105707266

# 16.6.5 定义/声明channel

【点击观看视频】管道快速入门案例
var 变量名 chan 数据类型
1
  • 举例:
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
2
3
4

说明

  • channel是引用类型
  • channel必须初始化才能写入数据, 即make后才能使用
  • 管道是有类型的,intChan 只能写入 整数 int

# 16.6.6 管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项

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
	// //如果从channel取出数据后,可以继续放入
	<-intChan
	intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量


	//4. 看看管道的长度和cap(容量)
	fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3

	//5. 从管道中读取数据

	var num2 int
	num2 = <-intChan 
	fmt.Println("num2=", num2)
	fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))  // 2, 3

	//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock

	num3 := <-intChan
	num4 := <-intChan

	//num5 := <-intChan

	fmt.Println("num3=", num3, "num4=", num4/*, "num5=", num5*/)

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# 16.6.7 channel使用的注意事项

【点击观看视频】管道的细节和课堂练习
  1. channel中只能存放指定的数据类型
  2. channle的数据放满后,就不能再放入了
  3. 如果从channel取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报deadlock

# 16.6.8 读写channel案例演示

1)创建一个intChan,最多可以存放3个int,演示存3数据到intChan,然后再取出这三个int

package main

import (
	"fmt"
)

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
	//num3 := <- intChan
	fmt.Printf("num1=%v num2=%v num3=%v", num1, num2, num3)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

2)创建一个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, 20)
	m1["city1"] = "北京"
	m1["city2"] = "天津"

	m2 := make(map[string]string, 20)
	m2["hero1"] = "宋江"
	m2["hero2"] = "武松"

	mapChan <- m1
	mapChan <- m2

	m11 := <-mapChan
	m22 := <-mapChan

	fmt.Println(m11, m22)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

3)创建一个catChan,最多可以存放10个Cat结构体变量

package main

import (
	"fmt"
)

type Cat struct {
	Name string
	Age  byte
}

func main() {
	var catChan chan Cat
	catChan = make(chan Cat, 10)
	cat1 := Cat{Name: "tom", Age: 18}
	cat2 := Cat{Name: "tom~", Age: 180}

	catChan <- cat1
	catChan <- cat2

	//取出
	cat11 := <-catChan
	cat22 := <-catChan

	fmt.Println(cat11, cat22)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

4)创建一个catChan2,最多可以存放10个*Cat变量,演示写入和读取的方法

package main

import (
	"fmt"
)

type Cat struct {
	Name string
	Age  byte
}

func main() {
	var catChan chan *Cat
	catChan = make(chan *Cat, 10)
	cat1 := Cat{Name: "tom", Age: 18}
	cat2 := Cat{Name: "tom~", Age: 180}

	catChan <- &cat1
	catChan <- &cat2

	//取出
	cat11 := <-catChan
	cat22 := <-catChan

	fmt.Println(cat11, cat22)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

5)创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法

package main

import (
	"fmt"
)

type Cat struct {
	Name string
	Age  byte
}

func main() {

	var allChan chan interface{}
	allChan = make(chan interface{}, 10)

	cat1 := Cat{Name: "tom", Age: 18}
	cat2 := Cat{Name: "tom~", Age: 180}

	allChan <- cat1
	allChan <- cat2
	allChan <- 10
	allChan <- "jack"

	//取出
	cat11 := <-allChan
	cat22 := <-allChan
	v1 := <-allChan
	v2 := <-allChan

	fmt.Println(cat11, cat22, v1, v2)

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

6)看下面代码,会输出什么?

package main

import (
	"fmt"
)

type Cat struct {
	Name string
	Age  byte
}

func main() {

	var allChan chan interface{}
	allChan = make(chan interface{}, 10)

	cat1 := Cat{Name: "tom", Age: 18}

	allChan <- cat1

	//取出
	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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 16.7 管道的课后练习题

说明:请完成如下案例

  • 创建一个Person结构体[Name,Age,Address]
  • 使用rand方法配合随机创建10个Person实例,并放入到channel中
  • 遍历channel,将各个Person实例的信息显示在终端

# 16.8 channel的遍历和关闭

【点击观看视频】管道的关闭和遍历

# 16.8.1 channel的关闭

使用内置函数close可以关闭channel, 当channel关闭后,就不能再向channel写数据了,但是仍然 可以从该channel读取数据

案例演示:

package main
import (
	"fmt"
)

func main() {
	intChan := make(chan int, 3)
	intChan<- 100
	intChan<- 200
	close(intChan) // close
	//这是不能够再写入数到channel
	//intChan<- 300
	fmt.Println("okook~")
	//当管道关闭后,读取数据是可以的
	n1 := <-intChan
	fmt.Println("n1=", n1)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 16.8.2 channel的遍历

channel支持for--range的方式进行遍历,请注意两个细节

1 ) 在遍历时,如果channel没有关闭,则回出现deadlock的错误

2 ) 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历

# 16.8.3 channel遍历和关闭的案例演示

package main
import (
	"fmt"
)

func main() {

	intChan := make(chan int, 3)
	intChan<- 100
	intChan<- 200
	close(intChan) // close
	//这是不能够再写入数到channel
	//intChan<- 300
	fmt.Println("okook~")
	//当管道关闭后,读取数据是可以的
	n1 := <-intChan
	fmt.Println("n1=", n1)


	//遍历管道
	intChan2 := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan2<- i * 2  //放入100个数据到管道
	}

	//遍历管道不能使用普通的 for 循环
	// for i := 0; i < len(intChan2); i++ {

	// }
	//在遍历时,如果channel没有关闭,则会出现deadlock的错误
	//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
	close(intChan2)
	for v := range intChan2 {
		fmt.Println("v=", v)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
【点击观看视频】协程配合管道的综合案例

# 16.8.4 应用实例 1

请完成goroutine和channel协同工作的案例,具体要求:

  1. 开启一个writeData协程,向管道intChan中写入50个整数
  2. 开启一个readData协程,从管道intChan中读取writeData写入的数据
  3. 注意:writeData和readData操作的是同一个管道
  4. 主线程需要等待writeData和readData协程都完成工作才能退出管道
  • 思路分析:

image-20210120154202844

  • 代码的实现:
package main
import (
	"fmt"
	"time"
)


//write Data
func writeData(intChan chan int) {
	for i := 1; i <= 50; i++ {
		//放入数据
		intChan<- i //
		fmt.Println("writeData ", i)
		//time.Sleep(time.Second)
	}
	close(intChan) //关闭
}

//read data
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, 10)
	exitChan := make(chan bool, 1)
	
	go writeData(intChan)
	go readData(intChan, exitChan)

	//time.Sleep(time.Second * 10)
	for {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 16.8.5 应用实例 2 - 阻塞

【点击观看视频】管道阻塞的机制

image-20210120154622723

# 16.8.6 应用实例 3

【点击观看视频】协程求素数的思路分析
  • 需求:

    要求统计 1 - 200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有goroutine和channel的知识后,就可以完成了 [测试数据: 80000 ]

  • 分析思路:

    传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。使用并发/并行的方式,将统计素数的任务分配给多个( 4 个)goroutine去完成,完成任务时间短。

  • 画出分析思路

image-20210120154848340

【点击观看视频】协程求素数的代码实现
【点击观看视频】协程求素数的代码效率测试
  • 代码实现
package main

import (
	"fmt"
	"time"
)

//向intChan放入1-8000个数
func putNum(intChan chan int) {
	for i := 1; i < 8000; i++ {
		intChan <- i
	}

	//关闭chan
	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, 1000)
	primeChan := make(chan int, 2000) //放入结果
	//标识退出的管道
	exitChan := make(chan bool, 4) //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
		}
		//当我们从exitChan取出了4个结果,就可以放心的关闭prprimeChan
		close(primeChan)
	}()

	//遍历我们的primeChan,把结果取出
	for {
		res, ok := <-primeChan
		if !ok {
			break
		}
		//将结果输出
		fmt.Printf("素数=%d\n", res)
	}

	fmt.Println("main线程退出")

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

结论:使用go 协程后,执行的速度,比普通方法提高至少 4 倍

# 16.9 channel使用细节和注意事项

【点击观看视频】管道的注意事项和细节(1)

1 ) channel可以声明为只读,或者只写性质 【案例演示】

package main
import (
	"fmt"
)

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)

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
【点击观看视频】管道的注意事项和细节(2)

2 ) channel只读和只写的最佳实践案例

package main

import (
	"fmt"
)

// 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("结束。。。")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

3 ) 使用select可以解决从管道取数据的阻塞问题

【点击观看视频】管道的注意事项和细节(3)
package main
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
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

4 ) goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题

image-20210120162547741

package main
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)
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  • 16.1 goroutine-看一个需求
  • 16.2 goroutine-基本介绍
  • 16.2.1 进程和线程介绍
  • 16.2.2 程序、进程和线程的关系示意图
  • 16.2.3 并发和并行
  • 16.2.4 Go协程和Go主线程
  • 16.3 goroutine-快速入门
  • 16.3.1 案例说明
  • 16.3.2 快速入门小结
  • 16.4 goroutine的调度模型
  • 16.4.1 MPG模式基本介绍
  • 16.4.2 MPG模式运行的状态 1
  • 16.4.3 MPG模式运行的状态 2
  • 16.5 设置GOLANG运行的CPU数
  • 16.6 channel(管道)-看个需求
  • 16.6.1 不同goroutine之间如何通讯
  • 16.6.2 使用全局变量加锁同步改进程序
  • 16.6.3 为什么需要channel
  • 16.6.4 channel的基本介绍
  • 16.6.5 定义/声明channel
  • 16.6.6 管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
  • 16.6.7 channel使用的注意事项
  • 16.6.8 读写channel案例演示
  • 16.7 管道的课后练习题
  • 16.8 channel的遍历和关闭
  • 16.8.1 channel的关闭
  • 16.8.2 channel的遍历
  • 16.8.3 channel遍历和关闭的案例演示
  • 16.8.4 应用实例 1
  • 16.8.5 应用实例 2 - 阻塞
  • 16.8.6 应用实例 3
  • 16.9 channel使用细节和注意事项