GO语言之Goroutine和channel

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 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值