go--协程

goroutine基本介绍

进程和线程说明

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

go协程和go主进程

  1. go主进程(有程序员直接称为线程/也可以理解为进程):一个Go程序上可以有多个协程,你可以理解携程是轻量级的线程
Go协程的特点
  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程

协程快速入门

案例

  1. 主线程(可以理解成进程)中,开启一个goroutine,该协程每隔一秒输出"Hello World"
  2. 在主线程中也隔每秒输出"Hello golang",输出10次后退出程序
  3. 要求主线程goroutine同时执行
package main

import (
	"fmt"
	"time"
	"strconv"
)

//在主线程(可以理解成进程)中,开启一goroutine,该协程每隔一秒输出"Hello World"
//在主线程中也隔每秒输出"Hello golang",输出10次后退出程序
//要求主线程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 golang" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

协程后的效果图
输出的效果说明.main主线程和test协程同时执行

小结
  • 可以定义一个普通函数,之后可以变成协程, 比如func test()
  • main()主函数(进程)中定义go 函数名(go test())来进行

MPG模式

  • M: 操作系统的主线程(是物理线程)
  • P: 协程执行需要的上下文
  • G: 协程
MPG模式运行状态1
  1. 当前程序有三个M,如果三个M都在一个CPU运行,就是并发,如果在不同CPU里面运行就是并行
  2. M1,M2,M3正在执行1G,M1的协程队列有三个;M2的协程有三个;M3的协程有俩个
  3. Go协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程
  4. 其他程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光CPU资源
MPG模式运行状态2
  1. 原来的情况是M0主线程执行Go协程,另外三个协程在队列等待
  2. 如果Go协程阻塞,比如读取文件或者数据库等
  3. 这个就会创建M1主线程(也可能是从已有的线程池中取出M1)并列将等待的3个线程挂在M1下开始执行,M0主线程下的Go仍然执行文件io读写

设置Golang运行CPU数

//获取当前系统cpu的数量
runtime.NumCPU()
//我们这里设置num-1的cpu运行go程序
runtime.GOMAXPROCS(num)
package main
import (
  "fmt"
  "runtime"
)
func main(){
  //获取当前系统cpu的数量
  num := runtime.NumCPU()
  //我们这里设置num-1的cpu运行go程序
  runtime.GOMAXPROCS(num)
  fmt.Println("num=",num)
}

全部CPU的图片

协程并发(并行)资源竞争问题

案例

  • 需求: 计算1-200的各各数的阶乘,并把各各数的阶乘放入map中,最后显示出来,要求使用goroutine完成
    1. 使用goroutine来完成效率高,但是会出现并发/并行安全问题
    1. 这里就提出了不同goroutine如何通讯的问题
package main

import (
	"fmt"
)

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

//1. 编写一个函数,来计算个数的阶乘并放入map中
//2. 我们启多个协程,统计的结果放入map中
//3. map应该做成全局的(因为都要访问到)

var (
	myMap = make(map[int]int,200)
)
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("map[%d]=%d\n",i,v)
	}
}

问题图片
以上这张图说明了,我们开启了协程,但是这个协程和我们的主线程是同一时间执行的,所以,map还没有赋值,主线程就已经执行到了for i,v := range myMap这种地方

go build -race main.go#这个也可以看到错误
全局互斥锁解决资源竞争问题
package sync//锁在这个包下

go-sync

package main

import (
	"fmt"
	"sync"
)

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

//1. 编写一个函数,来计算个数的阶乘并放入map中
//2. 我们启多个协程,统计的结果放入map中
//3. map应该做成全局的(因为都要访问到)

var (
	myMap = make(map[int]int,200)
	//lock是一个全局的互斥锁;sync是一个包:synchornized 同步
	//Mutex:互斥
	lock sync.Mutex
)
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)
	}
	lock.Lock()
	for i,v := range myMap {
		fmt.Printf("map[%d]=%d\n",i,v)
	}
	lock.Unlock()
}

加锁后的效果图

lock.Lock()
for i,v := range myMap {
	fmt.Printf("map[%d]=%d\n",i,v)
}
lock.Unlock()

这个地方加锁的原因是我们知道执行完所有的程序按理说不会有资源竞争问题,但是主线程并不知道,因此底层可能仍然会出现资源竞争,一次加入互斥锁可以解决(还是这个问题的话请加入掩饰(time.Sleep(10)))

channel(管道)

  • channel本质就是一个数据类型-队列
  • 数据是先进先出[FIFO(first in first out)]
  • 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全
  • channel是有类型的,一个string的channel只能存放string类型数据
快速入门

定义/声明

var 变量名 chan 数据类型

举例

var intChan chan int  //(intChan存放int数据类型)
var mapChan chan map[int]string //(mapChan 存放map[int]string数据类型)
var perChan chan Person //(perChan 存放struct数据类型)
var perChan2 chan *Person //(perChan2 存放指针数据类型)

说明

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

import (
	"fmt"
)


func main(){
	//创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int, 3)

	//看看intChan是什么
	fmt.Printf("intChan的值:%v,intChan本身:%v",intChan,&intChan)
}

在这里插入图片描述

在我们内存里面(0x00000a028)还存放了(0xc000020080)

向管道中添加数据
var 变量名 chan int
变量名<-int类型的数据
var intChan chan int
intChan <- 10
num := 211
intChan <- num
查看管道的长度和cap(容量)
len()
cap()
var intChan chan int
	intChan = make(chan int, 3)

	//看看intChan是什么
	// fmt.Printf("intChan的值:%v,intChan本身:%v",intChan,&intChan)

	//向管道写入数据
	intChan <- 10
	num := 211
	intChan <- num
	//查看管道的长度和cap(容量)
	fmt.Printf("channel len==%v,cap=%v\n",len(intChan),cap(intChan))

注意: 当我们给管道写入值的时,不要超过其容量
超过容量报错

取出管道中的数据
变量名 := <- 管道变量(比如intChan)
var intChan chan int
intChan = make(chan int, 3)
//向管道写入数据
intChan <- 10
num := 211
intChan <- num

num3 := <- intChan
num4 := <- intChan
在没有使用协程的情况下,如果我们的管道数据已全部取出,再取就会报deadlock
num3 := <- intChan
num4 := <- intChan
num5 := <- intChan//多余,管道中只有俩个数据

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

取出管道中没有的数据报错图

注意: 取出后在放入是没有问题的
num3 := <- intChan
num4 := <- intChan
intChan <- 20
num5 := <- intChan

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

在这里插入图片描述

取出输入,但是数据不想要
<- 管道名

例如

var intChan chan int
intChan <- 10
num := 211
intChan <- num
<-intChan //取出,但是数据没有变量接收
注意事项
  1. channel中只能存放指定数据类型(var intChan chan int只能存放int类型)
  2. channel数据放满后,就不能存放了(make(chan int,3)只能放3个,不然会报deadlock错误)
  3. 如果取从channel取出数据后,就可以继续放入了(只要不超过规定值(管道名<-数据存入)(<-管道名取出而且数据丢弃))
  4. 在没有使用协程的情况下,如果channel数据取完了,再取就会报deadlock错误
基本使用
int类型的管道
var intChan chan int
intChan = make(chan int,3)
intChan <- 10
intChan <- 20
intChan <- 10
num1 := <- intChan
num2 := <- intChan
num3 := <- intChan
fmt.Printf("num1=%v,num2=%v,num3=%v",num1,num2,num3)//10,20,10
map类型的管道
var mapChan chan map[string]string
mapChan = make(chan map[string]string,10)
m1 := make(map[string]string,20)
m1["city"] = "浙江"
m1["name"] = "admin1"

m2 := make(map[string]string,20)
m2["sex"] = "保密"
m2["addr"] = "成都"

mapChan <- m1
mapChan <- m2
结构体的管道
type Cat struct{
  Name string
  Age int
}
func main(){
  var catChan chan Cat
  catChan = make(chan Cat,10)
  cat1 := Cat{Name:"tom",Age:18,}
  cat2 := Cat{Name:"tom~",Age:30,}
  catChan <- cat1
  catChan <- cat2

  //取出
  cat11 := <- catChan
  cat22 := <- catChan
fmt.Println(cat11,cat22)
}
指针的管道
type Cat struct{
  Name string
  Age int
}
func main(){
  var catChan chan *Cat
  catChan = make(chan *Cat,10)
  cat1 := Cat{Name:"tom",Age:18,}
  cat2 := Cat{Name:"tom~",Age:30,}
  catChan <- &cat1
  catChan <- &cat2

  //取出
  cat11 := <- catChan
  cat22 := <- catChan
fmt.Println(cat11,cat22)
}
任意类型的管道
var allChan chan interface{}
allChan = make(chan interface{}, 10)

cat1 := Cat{Name:"tom",Age:18,}
cat2 := Cat{Name:"tom~",Age:30,}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"

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

fmt.Println(cat11.Name)//会报错,建议类型断言

没有使用类型断言

cat11 := <- allChan 
<- allChan
<- allChan
<- allChan

newCat := cat11.(Cat)//使用类型断言

fmt.Println(newCat.Name)

使用了类型断言

管道的关闭和遍历
channel关闭

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

package builtin//close在此包里

在这里插入图片描述

package main
import "fmt"

func main(){
  intChan := make(chan int,3)
  intChan <- 100
  intChan <- 200
  close(intChan)//close
  //这是不能够再写入数据到channel
  intChan <- 300
  fmt.Println("OK")
}

panic:send on closed channel

读是可以的

package main
import "fmt"

func main(){
  intChan := make(chan int,3)
  intChan <- 100
  intChan <- 200
  close(intChan)//close
  //读取数据是可以
  n1 := <- intChan
  fmt.Println("n1=",n1)
}

读channel-ok

channel遍历

channel支持for-range方式进行遍历,但是要注意俩点

  1. 在遍历时,如果channel没关闭,则会出现**deadlock错误**
  2. 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历后就会退出遍历
package main
import "fmt"

func main(){
  intChan := make(chan int,20)
  for i:= 1; i <= 20; i++ {
	intChan <- i * 2
  }
  close(intChan)
  for v := range intChan {
	fmt.Println("v=",v)
  }
}

协程(goroutine)配合管道(channel)的综合案例

应用实例1
  1. 开启一个writeData协程,向管道intChan写入50个整数
  2. 开启一个readData协程,从管道intChan读取writeData写入的值
  3. 注意: writeDatareadData操作的是同一个管道
  4. 主线程需要等待writeDatareadData协程都完成工作才退出
package main
import "fmt"

//WriteData
func WriteData(intChan chan int) {
	for i := 0; i < 50; i++ {
		//放入数据
		intChan <- i
	}
	close(intChan) //关闭channel,并不会影响读取数据
}

//ReadData
func ReadData(intChan chan int, exitChan chan bool){
	for {
		v,ok := <- intChan //从管道里面读
		if !ok {
			break
		}
		fmt.Printf("readData 读到的数据=%v\n",v)
	}
	//readData读取完数据(任务完成)
	exitChan <- true
	close(exitChan)
}

func main(){
	//创建俩管道
	intChan := make(chan int,50)
	//exitChan: 像是标志位
	exitChan := make(chan bool,1)
	//go func(intChan chan int){}
	go WriteData(intChan)
	go ReadData(intChan,exitChan)
}

以上这样的代码,会秒闪,主线程就退出了,主线程一旦退出,俩个协程也会跟着结束
所以我们可以使用time.Sleep(time.Second* 10),但是这不是我们所希望的

package main
import "fmt"

//WriteData
func WriteData(intChan chan int) {
	for i := 0; i < 50; i++ {
		//放入数据
		intChan <- i
	}
	close(intChan) //关闭channel,并不会影响读取数据
}

//ReadData
func ReadData(intChan chan int, exitChan chan bool){
	for {
		v,ok := <- intChan //从管道里面读
		if !ok {
			break
		}
		fmt.Printf("readData 读到的数据=%v\n",v)
	}
	//readData读取完数据(任务完成)
	exitChan <- true
	close(exitChan)
}

func main(){
	//创建俩管道
	intChan := make(chan int,50)
	//exitChan: 像是标志位
	exitChan := make(chan bool,1)
	//go func(intChan chan int){}
	go WriteData(intChan)
	go ReadData(intChan,exitChan)

	for {
		_, ok := <-exitChan
		if !ok{
			break
		}
	}
}
管道阻塞机制

以上代码如果把ReadData注释后,你一直写,你又不取,这个管道会因为你的数据多了没有取,所以会阻塞(管道的容量<你放的数据)

如果编译器(运行)发现一个管道只有写没有读,那么该管道就会阻塞。

写管道和读管道的频率不一致(无所谓)

管道的注意事项
  1. channel可以声明为只读或者只写性质
//声明为只写
var chan1 chan<-int
chan1 = make(chan<-int,3)
//声明只读
var chan2 <- chan int
num2 := <-chan2 //会报错因为没有make

最佳案例

//只写
func send(ch chan<-int){}
//只读
func recv(ch <- chan int){}
  1. 使用select可以解决从管道取数据的阻塞问题
    语法
select {
  case v := 管道:
  	语句
  ...
  default:
  	 语句
}
intChan := make(chan int,10)
for i := 0; i<10; i++ {
  intChan <- i
}

//传统方法在遍历管道时,如果不关闭会阻塞导致deadlock
//要是遇到的需求不确定什么时候关闭管道
//可以用select方式解决
for {
  select {
    //注意:这里如果管道一直没有关闭,不会一直阻塞而死锁
    //会自动到下一个case匹配
    case v:=<-intChan:  
      fmt.Printf("从intChan读取的数据%d\n",v)
    default:
      //可以加入任务逻辑
      break
  }
}
  1. foroutine中使用recover,解决协程出现的pina,导致程序崩溃问题
func test(){
  defer func(){
    if err := recover();err != nil {
      fmt.Println("test()发生了错误",err)
    }
  }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

结城明日奈是我老婆

支持一下一直热爱程序的菜鸟吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值