go学习之goroutine和channel

文章目录

一、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)画出主线程和协程执行流程图

代码实现

go 复制代码
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数目

go 复制代码
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加入互斥锁

go 复制代码
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 数据类型

举例

go 复制代码
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

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

go 复制代码
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案例演示

go 复制代码
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读取数据

go 复制代码
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已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历

代码演示

go 复制代码
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协程都完成工作才能退出

思路分析

看代码演示:

go 复制代码
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 --阻塞
go 复制代码
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.代码实现

go 复制代码
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可以声明为只读,或者只写性质
go 复制代码
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只读和只写的最佳实践案例
go 复制代码
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可以解决从管道取数据的阻塞问题
go 复制代码
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,导致程序崩溃问题
go 复制代码
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,进行处理,这些即使这个协程发生的问题,但是主线程任然不受影响,可以继续运行

相关推荐
StickToForever3 小时前
第4章 信息系统架构(五)
经验分享·笔记·学习·职场和发展
闲猫5 小时前
go orm GORM
开发语言·后端·golang
丁卯4045 小时前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
leegong231117 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql
Moonnnn.7 小时前
51单片机学习——动态数码管显示
笔记·嵌入式硬件·学习·51单片机
南宫生8 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode
技术小齐8 小时前
网络运维学习笔记 016网工初级(HCIA-Datacom与CCNA-EI)PPP点对点协议和PPPoE以太网上的点对点协议(此处只讲华为)
运维·网络·学习
竹言笙熙8 小时前
代码审计初探
学习·web安全
日记成书8 小时前
物联网智能项目
物联网·学习
虾球xz9 小时前
游戏引擎学习第118天
学习·游戏引擎