1,goroutine-看一个需求
需求:要求统计1-90000000000的数字中,哪些是素数哦?
分析思路:
1)传统的方法,就是使用一个循环,循环的判断各个数是不是素数。
2)使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成,这时就会用到goroutine
2,goroutine-基本介绍
2.1进程和线程介绍
2.2程序,进程和线程的关系示意图
2.3并发和执行
并发和并行
1)多线程程序在单核上运行就是并发
2)多线程程序在多核上运行就是并行
3)图示:
并发:一个cpu同时执行多个线程
并行:多个cpu上执行多个线程,就相当于一个cpu执行一个线程
2.4GO协程核GO主线程
GO主线程(有程序员直接称为线程/也可以理解为进程):一个GO线程上,可以有多个协程,你可以理解为协程是轻量级的线程[编译器做的优化]
GO协程的特点
1)有独立的栈空间
2)共享程序堆空间
3)调度由用户控制
4)协程是轻量级的线程
3,goroutine-简单例子
3.1案例说明
请编写一个程序,完成如下功能
1)在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔一秒输出一次helloworld“
2)在主线程中也每隔一秒输出”hellogolang“,输出10次后,退出程序
3)要求主线程和goroutine同时执行
4)画出主线程和协程的执行流程图
主线程和协程执行流程图
3.2小结
1)主线程是一个物理线程,直接作用在cpu上。是重量级的,非常耗费cpu资源
2)协程从主线程开启,是轻量级的线程,是逻辑态。对资源的耗费相对较小
3)golang的协程机制是重要的特点,可以轻松的开启上万个协程。其他语言的并发机制是一般基于线程的,开启过多的线程耗费资源大,这里就凸显出golang的优势了
4,goroutine的调度模型
4.1MPG模式基本介绍
1)M:操作系统的主线程(是物理线程)
2)P:协程执行需要的上下文
3)G:协程
4.2MPG模式运行的状态1
1)当前程序有三个M,如果三个M都在一个CPU运行,就是并发,如果在不同的CPU下运行就并行
2)M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有三个,M3的协程队列有两个
3)从上图可以看出:GO的协程是轻量级的线程,是逻辑态的,GO可以容易起上万个协程
4)其他程序c/java的多线程往往是内核态的,比较重量级,几千个线程可能耗光CPU
4.3MPG模式运行的状态2
1)分成两部分来看
2)原来的情况是M0主线程正在执行G0线程,另外有三个线程在队列等待
3)如果G0线程阻塞,比如读取文件或者数据库等
4)这时就会创建出m1主线程(也可能是从已经有的线程中取出M1)并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行io的读写
5)这样的MPG调度模式,可以既让G0执行同时也可以让队列的其他的协程执行,仍然可以并发执行
6)等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中获取),同时G0又会被唤醒
5,设置Golang运行的CPU数
说明:为了充分利用多cpu的优势,在Goalng程序中,设置cpu数目
6,channel(管道)-需求
需求:现在要计算1-200各个数的阶乘,并且把各个数的阶乘放到map中。最后显示出来,要求使用goroutine来完成
思路:
1)使用goroutine来完成,效率高,但是会出现并发/并行安全问题
2)这里就提出了不同goroutine如何通信的问题
代码实现:
1)使用goroutine来完成(看看goroutine并发完成会出现什么问题)
2)在运行某个程序时,如何知道是否存在资源竞争问题。在编译程序时,增加一个参数-race即可
package Goroutine
import (
"fmt"
"time"
)
var(
myMap=make(map[int]int,10)
)
//计算n!并放入到map里
func operation(n int) {
res:=1
for i:=1;i<=n;i++{
res*=i
}
myMap[n]=res
}
func Test3() {
//我们开启多个协程去完成这个任务
for i:=1;i<=200;i++{
go operation(i)
}
time.Sleep(time.Second*10)
fmt.Println(myMap)
}
会有错误产生
6.1不同goroutine之间如何通讯
1)全局变量互斥锁
2)使用channel来解决
6.2使用全局变量加锁同步改进程序
因为没有对全局变量加锁,因此会出现资源争夺的问题,代码会出现错误,提示
解决方案:加入互斥锁
我们的数阶乘很大,将数改为uint64()
package Goroutine
import (
"fmt"
"sync"
"time"
)
var(
myMap2=make(map[int]uint64,10)
//声明一个全局互斥锁,lock是一个全局互斥锁,sync是包:synchorized同步
//Mutex:是互斥
lock sync.Mutex
)
func operation2(n int) {
var res uint64=1
for i:=1;i<=n;i++{
res*=uint64(i)
}
//我们将res放入myMap
//加锁
lock.Lock()
myMap2[n]= res
//解锁
lock.Unlock()
}
func Test4() {
for i:=1;i<=200;i++{
go operation2(i)
}
time.Sleep(time.Second*10)
//这里我们输出
//加锁
lock.Lock()
fmt.Println(myMap2)
lock.Unlock()
}
6.3为什么需要channel
1)前面使用全局变量加锁同步来解决goroutine的通讯并不完美
2)主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,仅仅是估算
3)如果主线程休眠时间长了,会加长等待时间,如果时间短了,可能还有goroutine处于工作状态,这时也会随着主线程的退出而销毁
4)通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量进行读写操作
5)下面来讲以下channel
6.4 channel的基本介绍
1)channel本质是一个数据结构+队列
2)数据是先进先出
3)线程安全,多个goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
4)channel有类型的,一个string的channel只能存放string类型的数据
6.5定义/声明管道
var 变量名 chan 数据类型
举例:
var intChan chan int 存放int类型
var mapChan chan map[int]string mapChan存放map[int]string类型
var perChan chan Person...
说明
channel是引用类型
channel必须初始化才能写入数据,即make后才能用
管道是有类型的,intChan只能写入整数int
6.6管道的初始化,写入数据到管道,从管道读取数据,注意事项
package Goroutine
import "fmt"
func Test5() {
//演示一下管道的使用
//1,创建一个可以存放3个int类型的管道
var intChan chan int
intChan=make(chan int,3)
//2,看看intChan是什么
fmt.Printf("值:%V 地址:%p\n",intChan,&intChan)
//3,向管道写入数据
intChan<-10
num:=211
intChan<-num
intChan<-50
//intchan<-99注意不要超过它的容量
//4,看看管道的长度和cap(容量)
fmt.Printf("长度 len=%v cap=%v\n",len(intChan),cap(intChan))
//5,从管道里读取数据
var num2 int
num2=<-intChan
fmt.Println("num2=",num2)
fmt.Printf("长度 len=%v cap=%v\n",len(intChan),cap(intChan))
//6,在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告deadlock
num3:=<-intChan
num4:=<-intChan
num5:=<-intChan
fmt.Println("num3=",num3,"num4=",num4,"num5=",num5)
}
6.7channel使用的注意事项
1)channel只能存放指定的数据类型
2)channel的数据放满后,就不能再放入了
3)如果从channel取出数据后就可以继续放入
4)在没有使用协程的情况下,如果channel数据取完了,再取就会报错
6.8读写channel案例演示
1)创建一个intChan,最多可以放3个int,演示存三个数据到intChan然后再取出这三个int
2)创建一个mapChan,最多可以存放1个map[string]string演示写入和读取
3)创建一个结构体变量cat,创建一个管道,演示catChan的存取
4)创建一个allchan可以存放任意数据类型的变量
7,练习
1)创建一个person结构体[Name,Age,Address]
2)创建10个Person实例,并放入到channel中
3)遍历channel,将各个person实例信息显示在终端
package Goroutine
import "fmt"
type Person struct {
name string
Age int
Address string
}
func Test9() {
var PersonChan chan Person
PersonChan=make(chan Person,10)
for i:=0;i<10;i++{
PersonChan<-Person{"wang",i,"asa"}
}
for len(PersonChan)!=0{
fmt.Println(<-PersonChan)
}
}
8,chaneel的遍历和关闭
8.1channe的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
8.2channel的遍历
channel支持for-range的方式进行遍历
1)在遍历时,如果channel没有关闭,则会出现deadlock错误
2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后就会退出遍历
8.3channel遍历和关闭的案例演示
8.4应用实例1
请完成goroutine和channel协同工作的案例
1)开启一个writeData协程,向管道intChan中写入五十个数
2)开启一个readDate协程,从管道intChan中读取writeData写入数据
3)注意:WriteData和readData操作的是同一个管道
4)主线程需要等待writeData和readData协程都完成工作才能退出管道
package Goroutine
import (
"fmt"
)
//writeData
func writeData(intChan chan int) {
for i:=1;i<=50;i++{
intChan<-i
fmt.Println("writeData",i)
}
close(intChan)
}
//readData
func readData(intChan chan int,exitChan chan bool) {
for{
v,ok:=<-intChan
if !ok{
break
}
fmt.Println("readData",v)
//time.Sleep(time.Second)
}
exitChan<-true
close(exitChan)
}
func Test12() {
//创建两个管道
intChan:=make(chan int,50)
exitChan:=make(chan bool,1)
go writeData(intChan)
//time.Sleep(time.Second)
go readData(intChan,exitChan)
//time.Sleep(time.Second)
for{
_,ok:=<-exitChan
fmt.Println("sa")
if!ok{
break
}
}
}
8.5应用实例2-阻塞
如果只是向管道写入数据,而没有读取,就会出现阻塞而dead lock,原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在writeData的ch<-i
8.6应用实例3
要求统计1-200000的数字中,哪些是素数?我们这里采用goroutine和channnel来完成,测试数据是80000
分析思路:
传统的方法是一个循环,判断各个数字是不是素数
现在使用并发知识,将统计素数的任务分配给4个goroutine去完成
package Goroutine
import (
"fmt"
"time"
)
//向intChan放入1-8000个数字
func putNum(intChan chan int){
for i:=1;i<=8000;i++{
intChan<-i
}
//关闭intChan
close(intChan)
}
//从intChan中取出数据,并判断是不是素数,如果是就放入到primeChan
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
var flag bool
for{
time.Sleep(time.Millisecond*10)
num,ok:=<-intChan
if!ok{//intChan取不到
break
}
flag=true
for i:=2;i<num;i++{
if num%i==0{
flag=false
break
}
}
if flag{
//就将这个数放入到primeChan
primeChan<-num
}
}
fmt.Println("有一个primeNUM因为取不到数据,退出")
//向exitChan写入true
exitChan<-true
}
func Test13() {
intChan:=make(chan int,1000)
primeChan:=make(chan int,2000)//放入结果
//标识退出管道
exitChan:=make(chan bool,4)
go putNum(intChan)
//开启四个协程
for i:=0;i<4;i++{
go primeNum(intChan,primeChan,exitChan)
}
//这里我们主线程进行处理
//直接
go func() {
for i:=0;i<4;i++{
fmt.Println("exitchan",<-exitChan)
}
//当我们从exitChan中取出来四个结果后,就可以放心的关闭prprimeChan
close(primeChan)
}()
//遍历我们的primeChan
for{
res,ok:=<-primeChan
if !ok{
break
}
fmt.Println("素数",res)
}
fmt.Println("main线程退出")
}
9,channel使用细节和注意事项
1)channel可以声明为只读,或者只写性质
2)channel只读和只写的最佳实践案例
3)使用select可以解决从管道读取数据的阻塞问题
package Goroutine
import (
"fmt"
"time"
)
func Test15() {
//1,定义一个管道存10个数据
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<-"sww"+fmt.Sprintf("%d",i)
}
//在传统的方法中我们不关闭会阻塞而导致deadlock
//我们可以使用select方式解决
for {
select{
//注意,这里如果intchan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
case v:=<-intChan:
fmt.Println("数据",v)
time.Sleep(time.Second)
case v:=<-stringChan:
fmt.Println("字符串",v)
time.Sleep(time.Second)
default:
fmt.Println("都取不到了")
return
}
}
}
4)goroutine中使用recover解决协程中出现的panic,导致程序出现问题,这时我们可以在goroutine中使用recover来捕获panic进行处理,这样即使这个协程发生问题,但是主线程仍然不受影响,可以继续执行
package Goroutine
import (
"fmt"
"time"
)
func sayHello() {
for i:=0;i<10;i++{
time.Sleep(time.Second)
fmt.Println("hello world")
}
}
//函数
func errorrecover(){
//这里我们可以使用defer+recover
defer func() {
//捕获test抛出的panic
err:=recover()
if err!=nil{
fmt.Println("test()发生错误",err)
}
}()
var m1 map[int]string
m1[0]="ss"
}
func Test16() {
go sayHello()
go errorrecover()
for i:=0;i<10;i++{
fmt.Println(i)
time.Sleep(time.Second)
}
}
码字不易,还望点个赞,点个关注多多支持一下!谢谢!
(免费订阅,永久学习)学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂
更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,永久学习,或点击这里加qun免费
领取,关注我持续更新哦! !
原文链接;https://zhuanlan.zhihu.com/p/419186816