文章目录
- 一、goroutine(协程)
- 1.goroutine入门
- 2.goroutine基本介绍
- -1.进程和线程说明
- -2.程序、进程和线程的关系示意图
- -3.Go协程和Go主线程
- 3.案例说明
- 4.小结
- 5.MPG模式基本介绍
- 6.设置Golang运行的CPU数
- 7.协程并发(并行)资源竞争的问题
- 8.全局互斥锁解决资源竞争
- 二、管道
- 1.为什么要使用channel
- 2.channel的介绍
- 3.管道的定义/声明channel
- 4.channel使用的注意事项
- 5.读写channel案例演示
- 6.channel的遍历和关闭
- -1.channel的关闭
- -2.channel的遍历
- 7.管道阻塞的机制
- -1.应用实例2 --阻塞
- 2-应用实例3
- 8.channel使用细节和注意事项
- 1)channel可以声明为只读,或者只写性质
- 2)channel只读和只写的最佳实践案例
- 3)使用select可以解决从管道取数据的阻塞问题
- 4)goroutine中使用recover。解决协程中出现panic,导致程序崩溃问题
一、goroutine(协程)
1.goroutine入门
需求:要求统计1-20000的数字中,哪些是素数
分析思路
1)传统的方法,就是使用一个循环,循环的判断各个数是不是素数
2)使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成。这是就会使用到goroutine去完成,这时就会使用goroutine
2.goroutine基本介绍
-1.进程和线程说明
1)进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
2)线程是进程的一个执行实例吗,是程序执行的最小单位,他是比进程更小的能独立运行的基本单位
3)一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
4)一个程序至少有一个进程,一个进程至少有一个线程
-2.程序、进程和线程的关系示意图
-3.并发和并行
1)多线程程序在单核上运行,就是并发
2)多线程程序在多核上运行,就是并行
并发:因为是在一个CPU上,比如有10个线程,每个线程执行10毫秒(进行轮换操作),从人的角度来看,好像这10个线程都在运行,但是从微观上看,在某一个时间点来看,其实只有一个线程在执行,这就是并发
并行:因为是在多个CPU上(比如有10个CPU),比如有10个线程,每个线程执行10毫秒(各自在不同的CPU上执行),从人的角度上看,这10个线程都在运行,但是从微观上看,在某一个时间点,也是同时有10个线程在执行,这就是并行
-3.Go协程和Go主线程
1)Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程
2)Go协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
示意图
3.案例说明
请编写一个程序,完成如下功能
1)在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔1秒输出"hello world"
2)在主线程也每隔一秒输出"hello golang",输出10次后,退出程序
3)要求主线程和goroutine同时执行
4)画出主线程和协程执行流程图
代码实现
package main
import (
"fmt"
"strconv"
"time"
)
/*
1)在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔1秒输出"hello world"
2)在主线程也每隔一秒输出"hello golang",输出10次后,退出程序
3)要求主线程和goroutine同时执行
*/
//编写一个函数,每隔1秒输出"hello world
func test () {
for i := 1; i <= 10; i++ {
fmt.Println("test()hello world"+strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() //开启了一个协程
for i := 1; i <= 10; i++ {
fmt.Println("main()hello world"+strconv.Itoa(i))
time.Sleep(time.Second)
}
}
执行结果如下,我们可以发现主线程和go协程是同时执行的
go主线程与go协程的执行示意图
4.小结
1)主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常消耗cpu资源,
2)协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对少
3)golang的协程机制是重要的特点,可以轻松开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就凸显出golang在并发上的优势了
5.MPG模式基本介绍
1)M:操作系统的主线程(是物理线程)
2)P:协程执行需要的是上下文
3)G:协程
6.设置Golang运行的CPU数
介绍:为了充分利用多cpu的优势,在Golang程序中,设置运行的cpu数目
package main
import (
"fmt"
"runtime"
)
func main() {
//获取当前系统CPU的数量
num := runtime.NumCPU()
//我这里设置num -1的cpu运行go程序
runtime.GOMAXPROCS(num)
fmt.Println("num=",num)
}
1)go1.8后,默认让程序运行在多个核上,可以不用设置了
2)go1.8前,还是要设置一下,可以更高效的利用CPU了
7.协程并发(并行)资源竞争的问题
需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中,最后显示出来。要求使用goroutine完成
分析思路:
1)使用goroutine来完成,效率高,但是会出现并发/并行安全问题
2)这里就提出了不同1goroutine如何通信的问题
代码实现
1)使用goroutine来完成(看看使用goroutine并发完成会出现什么问题?
2)在运行某个程序时,如何知道是否存在资源竞争的问题,方法很简单。在编译该程序时增加一个参数 -race即可
会发现map有些有值有些没有值,各个协程出现了资源竞争的问题
3)示意图
他们之间会出现资源竞争的问题
8.全局互斥锁解决资源竞争
不同的goroutine之间如何通信
1)全局变量加锁同步
2)channel
使用全局变量加锁同步改进程序
因为没有针对全局变量m加锁,因此会出现资源竞争的问题,代码会出现报错提示concurrent map writes
解决方案,-1加入互斥锁
package main
import (
"fmt"
"time"
"sync"
)
//需求:现在要计算1-200的各个数的阶乘,
// 并且把各个数的阶乘放入到map中,最后显示出来。要求使用goroutine完成
//思路
//1.编写一个函数,来计算各个数的阶乘,并放入到map中
//2.我们爱动的协程是多个,统计的结果放入到map中
//2.map应该做出一个全局的
var (
myMap = make(map[int]int,10)
//声明一个全局的互斥锁
//lock是一个全局的互斥锁
//sync 是包:synchornized 同步
//Mutex是互斥的意思
lock sync.Mutex
)
//test函数就是计算n的阶乘,将这个结果放入到map中
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 <=15; 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()
}
我们的数的阶乘很大,结果会越界,我们可以改成sum +=uint64(i)
加锁解释
二、管道
1.为什么要使用channel
前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
1)主线程在等待所有goroutine全部完成的时间很难确定。我们这里设置10秒,仅仅只是估算
2)如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随着主线程的退出而销毁
3)通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作
4)上面的种种分析都在呼唤一个新的通讯机制-channel
2.channel的介绍
1)channel本质就是一个数据结构-队列
2)数据是先进先出[FIFIO frist in first out]
3)线程安全,多goroutine访问时,不需要加锁,就是说在channel本身就是线程安全的
4)channel是有类型的,一个string的channel只能存放string数据
channel是线程安全,多个协程作同一个管道时,不会发生资源竞争的问题
3.管道的定义/声明channel
var 变量名 chan 数据类型
举例
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)channel是引用类型
2)channel必须初始化才能写入数据,即make后才能使用
3)管道是有类型的 intChan只能写入整数int
管道的初始化,写入数据到管道,从管道读取数据以及基本的注意事项
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
//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.Printf("num2=%v\n",num2) //10
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)//报错
}
4.channel使用的注意事项
1)channel中只能存放指定的数据类型
2)channel的数据放满后,就不能在放入了
3)如果从channel取出数据后,可以继续放入
4)在没有使用协程的情况下,如果channel数据取完了,再取就会报deadlock
5.读写channel案例演示
package main
import (
"fmt"
)
type Cat struct {
Name string
Age int
}
func main() {
//定义一个存放任意数据类型的管道3个数据
// var allChan chan interface{}
allChan := make(chan interface{},3)
allChan<-10
allChan<-"tom jack"
cat :=Cat{"小花猫",4}
allChan<- cat
//我们希望获得管道中的第三个元素,则先将前2个推出
<-allChan
<-allChan
newCat :=<-allChan //从管道中取出来的cat是什么
fmt.Printf("newCat=%T,newCat=%v\n",newCat,newCat)//newCat=main.Cat,newCat={小花猫 4}
//下面的写法是错误的,编译不通过,则使用类型断言就可以通过
// fmt.Printf("newCat.Name=%v",newCat.Name)
a :=newCat.(Cat)
fmt.Printf("newCat.Name=%v",a.Name)//newCat.Name=小花猫
}
6.channel的遍历和关闭
-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 //panic: send on closed channel
fmt.Println("okok~")
//当管道关闭后,读取数据是可以的
n1 := <-intChan
fmt.Println("n1=",n1)
//输出如下
// okok~
//n1= 100
}
-2.channel的遍历
channel支持for-range的方式进行遍历,请注意两个细节
1)在遍历时,如果channel没有关闭,则会出现deadlock的错误
2)在遍历时,如果cahnnel已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历
代码演示
package main
import (
"fmt"
)
func main() {
intChan :=make(chan int,3)
intChan<- 100
intChan<- 200
close(intChan) //close
//这时不能够再写入到数channel
//intChan<- 300 //panic: send on closed channel
fmt.Println("okok~")
//当管道关闭后,读取数据是可以的
n1 := <-intChan
fmt.Println("n1=",n1)
//遍历管道
intChan2 :=make(chan int,100)
for i :=0; i < 100; i++ {
intChan2 <- i *2 //放入100个数据进去管道之中
}
//遍历:这种遍历是错误的,因为遍历过程中管道的长度会变化
// for i :=0; i < len(intChan2);++ {
// }
//在遍历时,如果channel没有关闭,则回出现deadlock的错误
//在遍历时,如果cahnnel已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历
close(intChan2)
for v := range intChan2 {
fmt.Println("v=",v)
}
}
7.应用案例
-1.应用案例1
请完成goroutine和channel协同工作案例,具体要求
1)开启一个writeData协程,向管道intChan中写入50个整数
2)开启一个readData协程,从管道inChan中读取writeData写入的数据
3)注意:writeData和readData操作的是同一个管道
4)主线程需要等到writeData协程都完成工作才能退出
思路分析
看代码演示:
package main
import (
"fmt"
_"time"
)
//writeDtata
func writeData(intChan chan int) {
for i :=0;i<=50;i++ {
//放入数据
intChan<- i
fmt.Println("writeData",i)
// time.Sleep(time.Second )
}
close(intChan)//关闭管道,不影响读
}
readDtata
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 //数据读取完之后就网退出管道加入一个1
close(exitChan)
}
func main() {
//创建两个管道
intChan := make(chan int,50)
exitChan :=make(chan bool,1 )
go writeData(intChan)
go readData(intChan,exitChan)
//time.Sleep(time.Second * 10)
for {
_, ok :=<-exitChan
if !ok {
break
}
}
}
7.管道阻塞的机制
-1.应用实例2 --阻塞
func main() {
intChan :=make(chan int, 10) //10->50的话数据一下就放入了
exitChan :=make(chan bool,1)
//go readData(intChan,exitChan)
//就是为了等待readData的协程完成
for ——=range exitChan{
fmt.Println("ok...")
}
}
问题:如果注销掉go readData(intChan, exitChan)程序会怎么样
答:如果只是向管道写入数据,而没有读取,就会出现阻塞而deadLock,原因是intChan容量是10,而writeData会写入50个数据,因此会阻塞在writeData的ch <-i
2-应用实例3
1)需求:要求统计1 200000的数字中,哪些是素数?这个问题在本章开篇就提出了,
现在我们有goroutine和channel的知识后,就可以完成了[测试数据:80000]
2)分析思路:
-
传统的方法,就是使用一个循环,循环的判断各个数是不是素数。
-
使用并发/并行的方式,将统计素数的任务分配给多个(4个)goroutine去完成,
完成任务时间短。
1.画出分析思路
2.代码实现
package main
import (
"fmt"
"time"
)
//向intChan放入 1-8000个数
func putNum(intChan chan int){
for i := 0 ;i<8000; i++{
intChan<- i
}
//关闭intChan
close(intChan)
}
//从intchan中取出数据,并判断是否为素数,如果是就放入到primeChan
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
//使用for循环
var flag bool
for {
time.Sleep(time.Millisecond)
num,ok := <-intChan
if !ok { //intChan取不到的时候,就退出这个主for循环
break
}
flag = true //假设是素数
//判断num是不是素数
for i :=2;i<num;i++{
if num %i ==0 { //说明i不是素数
flag = false
break
}
}
if flag {
//将这个数就放入到primeChan中
primeChan<- num
}
}
fmt.Println("有一个协程因为取不到数据没退出!")
//这里我们还不能关闭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个结果,就可以放心关闭primeChan
close(primeChan)
}()
//遍历primeChan
for {
res,ok := <-primeChan
if !ok {
break
}
//将结果输出
fmt.Printf("素数为=%d\n",res)
}
fmt.Println("main主线程退出")
}
说明:使用goroutine完成后,可以在使用传统的方法来统计一下,看看完成这个
任务,各自耗费的时间是多少?[用map保存primeNum]
使用go协程后,执行的速度,比普通方法提高至少4倍
8.channel使用细节和注意事项
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 err在这个管道中不可以读
fmt.Println("chan2=",chan2)
//3.声明为只读
var chan3 <-chan int
num2 := <-chan3
// chan3<- 30 err 会报错,因为该管道为只读
fmt.Println("num2=",num2)
}
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("结束...")
}
3)使用select可以解决从管道取数据的阻塞问题
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 {
case v := <-intChan : //注意:这里如果 intChan一直没有关闭,不会导致deadlocks,会自动到下一个case
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.Println("都取不到,不玩了,你可以加入逻辑")
time.Sleep(time.Second)
return
break label
}
}
}
4)goroutine中使用recover。解决协程中出现panic,导致程序崩溃问题
package main
import (
"fmt"
"time"
)
//函数1
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello world")
}
}
//函数2
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" //erro
}
func main(){
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=",i)
time.Sleep(time.Second)
}
}
输出结果如下
说明:如果我们起了一个协程,但…是这个协程出现了panic,如果我们没有捕获这个panic。就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic,进行处理,这些即使这个协程发生的问题,但是主线程任然不受影响,可以继续运行