golang学习笔记015--goroutine和channel

golang学习笔记015--goroutine和channel,第1张

目录 1.goroutine1.1 进程和线程介绍1.2 并行和并发1.3 Go协程和Go主线程1.4 goroutine快速入门(1)案例说明(2)代码实现(3)主线程和协程执行流程图(4)快速入门小结 1.5 goroutine的调度模型(1)MPG模式基本介绍(2)MPG模式运行的状态1(3)MPG模式运行的状态2 1.6 设置Golang运行的cpu数 2.channel2.1 需求2.2初步实现,但有问题2.3 不同goroutine之间如何通讯2.4 使用全局变量加锁同步改进程序2.5 为什么需要channel2.6 Channel的基本介绍2.7 定义/声明channel2.8 管道初始化2.9 读写channel案例2.10 channel的关闭2.11channel的遍历2.12 应用实例2.13阻塞2.14 应用goroutine和channel2.15 channel只读或只写2.16 使用select可以解决从管道取数据的阻塞问题2.17 goroutine中使用recover解决出现panic问题

1.goroutine 1.1 进程和线程介绍

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

1.2 并行和并发

● 多线程程序在单核上运行,就是并发
● 多线程程序在多核上运行,就是并行

● 并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询 *** 作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
● 并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

1.3 Go协程和Go主线程
● Go主线程(有程序员直接称为线程/也可理解为进程):一个Go线程上,可以起多个协程,可以这样理解,协程是轻量级的线程【编译器做优化】
● Go协程的特点
  ○ 有独立的栈空间
  ○ 共享程序堆空间
  ○ 调度由用户控制
  ○ 协程是轻量级的线程
1.4 goroutine快速入门 (1)案例说明

编写一个程序,完成如下功能:
在主线程(可以理解为进程)中,开启一个goroutine,该协程每隔1秒输出“hello,world”
在主线程也每隔一秒输出“hello,golang”,输出10次后,退出程序
要求主线程和goroutine同时执行

(2)代码实现
	package main
	
	import (
		"fmt"
		"strconv"
		"time"
	)
	
	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)
		}
	}

(3)主线程和协程执行流程图

(4)快速入门小结

● 主线程是以一个物理线程,直接作用在cpu上。是重量级的,非常消耗cpu资源
● 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对较小
● golang的协程机制是个重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就凸显出golang在并发上的优势了

1.5 goroutine的调度模型 (1)MPG模式基本介绍


M: *** 作系统的主线程(是物理线程)
P:协程执行需要的上下文
G:协程

(2)MPG模式运行的状态1


● 当前程序有三个M,如果三个M都在一个cpu运行,就是并发,如果在不同的cpu运行就是并行
● M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有3个,M3协程队列有2个
● 从上图可以看到: Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程。
● 其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu

(3)MPG模式运行的状态2

● 分成两个部分来看
● 原来的情况是MO主线程正在执行G0协程,另外有三个协程在队列等待
● 如果GO协程阻塞,比如读取文件或者数据库等,这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行, MO的主线程下的GO仍然执行文件io的读写。
● 这样的MPG调度模式,可以既让GO执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。
● 等到G0不阻塞了,MO会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒。

1.6 设置Golang运行的cpu数
	package main
	
	import (
		"fmt"
		"runtime"
	)
	
	func main() {
		//获取当前cpu数 返回的是本地机器的逻辑cpu数
		num := runtime.NumCPU()
		//设置运行go程序cpu数
		runtime.GOMAXPROCS(num - 1)
		fmt.Println("num=", num)
	}


● go1.8后,默认让程序运行在多个核上,可以不用设置了
● go1.8前,需要设置

2.channel 2.1 需求

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

2.2初步实现,但有问题
	package main
	
	import "fmt"
	
	var myMap = make(map[int]int, 10)
	
	//test函数用来计算阶乘并将结果放入到map中
	func test(n int) {
		res := 1
		for i := 1; i <= n; i++ {
			res *= i
		}
	
		myMap[n] = res
	}
	
	func main() {
		//开启多个协程完成这个任务【200个】
		for i := 1; i <= 200; i++ {
			go test(i)
		}
	    
		//休眠10秒钟
		time.Sleep(time.Second * 10)
	    
		//输出结果
		for i, v := range myMap {
			fmt.Printf("map[%d]=%d\n", i, v)
		}
	}

直接运行报错,因为对map并发的写

如何判断程序是否存在资源竞争问题,在编译程序时,增加一个参数 -race 即可
go build -race main.go
如果存在资源竞争问题,会打印出来
如果不休眠,程序运行不完整

2.3 不同goroutine之间如何通讯

● 全局变量的互斥锁
● 使用管道channel来解决

2.4 使用全局变量加锁同步改进程序

● 因为对全局变量myMap没有加锁,因此会出现资源争夺问题,代码会出现错误,提示fatal error: concurrent map writes错误
● 解决方法:加入互斥锁

	package main
	
	import (
		"fmt"
		"sync"
	)
	
	var (
		myMap = make(map[int]int, 10)
	
		//声明一个全局的互斥锁
		//lock 是一个全局的互斥锁
		//sync 是包
		//Mutex 是互斥
		lock sync.Mutex
	)
	
	//test函数用来计算阶乘并将结果放入到map中
	func test(n int) {
		res := 1
		for i := 1; i <= n; i++ {
			res *= i
		}
	
		//加锁
		lock.Lock()
		myMap[n] = res
		//解锁
		lock.Unlock()
	}
	
	func main() {
		//开启多个协程完成这个任务【200个】
		for i := 1; i <= 200; i++ {
			go test(i)
		}
	
	    //休眠10秒钟
		time.Sleep(time.Second * 10)
	    
		//输出结果
		//加锁
		lock.Lock()
		for i, v := range myMap {
			fmt.Printf("map[%d]=%d\n", i, v)
		}
		//解锁
		lock.Unlock()
	}

没有报错

为什么输出结果也要加锁呢?
因为主线程不知道协程什么时候执行完,因此底层可能仍然出现资源争夺
map[n]的结果为0,是因为结果超出int范围了

2.5 为什么需要channel

● 前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
● 主线程在等待所有goroutine全部加载完成的时间很难确定,我们这里设置10秒,仅仅是估算
● 如果主线程休眠时间长了,会加长等待时间,如果的等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
● 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写 *** 作

2.6 Channel的基本介绍

● channel本质就是一个数据结构-队列
● 数据是先进先出
● 线程安全,多个goroutine访问时,不需要加锁,channel本身是线程安全的
● channel有类型,一个string的channel只能存放string类型的数据

2.7 定义/声明channel
var 变量名 chan 数据结构

举例:
var intChan chan int
var mapChan chan map[int]string
var perChan chan Person
var perChan2 chan *Person

说明:
channel是引用类型
channel必须初始化才能写入数据,即make后才能使用
管道是有类型的,intChan只能写入整数int
2.8 管道初始化
	package main
	
	import "fmt"
	
	func main() {
	
		//1.创建一个可以存放三个int类型的管道
		var intChan chan int
		intChan = make(chan int, 3)
	
		//2.intChan是什么
		fmt.Printf("intChan的值是=%v\n", intChan) //intChan的值是=0xc0000d4080
	
		//3.向管道写入数据
		intChan <- 19
		intChan <- 12
		intChan <- 23
		//intChan<-50//报错,当我们给管道写入数据时不能超过其容量
	
		//4.管道长度和容量
		fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan)) //channel len=3 cap=3
	
		//5.从管道中读取数据
		var num int
		num = <-intChan
		fmt.Println("num=", num)                                          //num= 19
		fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan)) //channel len=2 cap=3
	
		//6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取出就会报错deadlock
		num2 := <-intChan
		num3 := <-intChan
		//num4 := <-intChan//报错
	
		fmt.Println(num2, num3) //12 23
	}

注意事项:
● channel中只能存放指定的数据类型
● channel数据放满后,就不能再放了
● 如果从channel取出数据后,可以继续放
● 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock

2.9 读写channel案例





注意:

	package main
	
	import "fmt"
	
	type Cat struct {
		Name string
		Age  int
	}
	
	func main() {
		var allChan chan interface{}
		allChan = make(chan interface{}, 10)
	
		cat1 := Cat{Name: "tom", Age: 10}
		cat2 := Cat{Name: "tom~~", Age: 20}
	
		allChan <- cat1
		allChan <- cat2
		allChan <- 121
		allChan <- "abc"
	
		//读取
		cat111 := <-allChan
		cat222 := <-allChan
		v1 := <-allChan
		v2 := <-allChan
	
		fmt.Println(cat111, cat222, v1, v2) //{tom 10} {tom~~ 20} 121 abc 这样输出没有问题
		//fmt.Println("Cat Name:",cat111.Name)//编译就报错cat111.Name undefined (type interface{} has no field or method
		//如果要拿到name,必须使用断言
		a := cat111.(Cat)
		fmt.Println("Cat Name:", a.Name) //Cat Name: tom
	}
2.10 channel的关闭

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

	package main
	
	import "fmt"
	
	func main() {
		intChan := make(chan int, 3)
		intChan <- 23
		intChan <- 34
		close(intChan)
		//关闭后如果写入会报错
		//intChan <- 45  //panic: send on closed channel
		// 关闭后可以正常读取数据
		n1 := <-intChan
		fmt.Println(n1) //23
	
	}
2.11channel的遍历

channel支持for-range的方式进行遍历。需要注意两点
● 在遍历时,如果channel没有关闭,则会出现deadlock的错误
● 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历

	package main
	
	import "fmt"
	
	func main() {
	    
		intChan1 := make(chan int, 100)
		for i := 1; i <= 100; i++ {
			intChan1 <- i * 2
		}
		//遍历管道不能用普通的for循环,因为长度一直在变化
	
		//在遍历时,如果channel没有关闭,则会出现deadlock的错误
		// 打印完数据并报fatal error: all goroutines are asleep - deadlock!
		//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历
		close(intChan1)
		for v := range intChan1 {
			fmt.Println("v=", v)
		}
	
	}
2.12 应用实例

使用goroutine和channel协同工作完成案例
要求:
● 开启一个writeData协程,向管道intChan中写入50个整数
● 开启一个readData协程,向管道intChan中读取writeData写入的数据
● 注意:writeData和readData *** 作的是同一个管道
● 主线程需要等待writeData和readData协程都完成后才能退出
思路分析:

	package main
	
	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)
		}
		//数据读取完成后,即任务完成
		exitChan <- true
		close(exitChan)
	}
	
	func main() {
	
		//创建2个管道
		intChan := make(chan int, 10)
		exitChan := make(chan bool, 1)
		go writeData(intChan)
		go readData(intChan, exitChan)
	
		for {
			_, ok := <-exitChan
			if !ok {
				break
			}
		}
	}
2.13阻塞

如果将上面代码的读数据注释go readData(intChan, exitChan),就会发生阻塞
运行结果:

writeData: 1
writeData: 2
writeData: 3
writeData: 4
writeData: 5
writeData: 6
writeData: 7
writeData: 8
writeData: 9
writeData: 10
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:37 +0x92

goroutine 19 [chan send]:
main.writeData(0x0)
        C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:8 +0x3b
created by main.main
        C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:33 +0x86
exit status 2

如果程序运行发现一个管道只有写,没有读,则该管道会发生阻塞。写管道和读管道的频率不一致无所谓

2.14 应用goroutine和channel

需求:统计1-8000中,哪些是素数?
思路:
● 传统:使用一个循环,循环判断
● 使用并发/并行:将统计素数的任务分配给多个goroutine去完成,完成任务时间短

	package main
	
	import (
		"fmt"
	)
	
	//将1-8000写入管道
	func putNum(intChan chan int) {
		for i := 1; i <= 8000; i++ {
			intChan <- i
		}
		close(intChan)
	}
	
	//求素数,如果是就放到primeChan管道
	func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
		var flag bool
		for {
			num, ok := <-intChan
			if !ok { //取不到了
				break
			}
			flag = true
			//判断num是不是素数
			for i := 2; i < num; i++ {
				if num%i == 0 { //说明概述不是素数
					flag = false
					break
				}
			}
			if flag {
				primeChan <- num
			}
		}
	
		fmt.Println("有一个primeNu协程因为取不到数据退出!!!")
		exitChan <- true
	}
	
	func main() {
		intChan := make(chan int, 1000)
		primeChan := make(chan int, 2000)
		exitChan := make(chan bool, 4)
	    
	    start := time.Now().Unix()
	
		//开启一个协程,向intChan中放入1-80000
		go putNum(intChan)
		//开启四个协程,从intChan取出数据,并判断是否为素数,如果是就放到primeChan
		for i := 0; i < 4; i++ {
			go primeNum(intChan, primeChan, exitChan)
		}
	
		//主线程直接处理
		go func() {
			for i := 0; i < 4; i++ {
				<-exitChan
			}
	        end := time.Now().Unix()
			fmt.Println("使用协程耗时=", end - start)
			//当我们从exitChan取出4个结果,就可以放心的关闭primeChan
			close(primeChan)
		}()
	
		//遍历primeChan,取出结果
		for {
			res, ok := <-primeChan
			if !ok {
				break
			}
			fmt.Println("素数=", res)
		}
		fmt.Println("main()线程退出")
	}

使用4个协程后,执行速度比普通方法提高至少4倍

2.15 channel只读或只写
	package main
	
	import "fmt"
	
	func main() {
		//管道可以声明为只读或只写
	
		//1.在默认情况下,管道是双向
		//var channel chan int //可读可写
	
		//声明为只写
		var chan2 chan<- int
		chan2 = make(chan int, 3)
		chan2 <- 20
		//num:=<-chan2//报错invalid operation: cannot receive from send-only channel chan2 (variable of type chan<- int)
		fmt.Println("chan2=", chan2)
	
		//声明为只读
		var chan3 <-chan int
		num2 := <-chan3
		// chan3<-10//报错
		fmt.Println("num2=", num2)
	}
2.16 使用select可以解决从管道取数据的阻塞问题
	package main
	
	import (
		"fmt"
		"strconv"
	)
	
	func main() {
		//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" + strconv.Itoa(i)
		}
		//label
		//传统的方法在遍历管道时,如果不关闭会阻塞而导致死锁deadlock
		//在实际开发中,我们不好确定什么时候关闭管道,可以用select方式解决
		for {
			select {
			//如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
			case v := <-intChan:
				fmt.Println("从intChan读取的数据:", v)
			case v := <-stringChan:
				fmt.Println("从stringChan读取的数据:", v)
			default:
				fmt.Println("都取不到数据:")
				return
				//或使用标签 break label
			}
		}
	}

2.17 goroutine中使用recover解决出现panic问题

如果一个协程出现了panic,如果没有捕获这个panic就会造成整个程序崩溃
这时我们需要在协程中使用revover来捕获panic进行处理,这样即使这个协程发生问题,主线程不会受影响,可以继续执行

	package main
	
	import (
		"fmt"
		"time"
	)
	
	func sayHello() {
		for i := 0; i < 10; i++ {
			fmt.Println("hello,world", i)
		}
	}
	
	func test() {
		//定义一个map,没有make就赋值
		var myMap map[int]string
		myMap[0] = "golang" //报错
	}
	
	func main() {
		go sayHello()
		go test()
		
		for i := 0; i < 10; i++ {
			fmt.Println("main()ok", i)
			time.Sleep(time.Second)
		}
	}

不处理错误,程序报错并停止

解决

	package main
	
	import (
		"fmt"
		"time"
	)
	
	func sayHello() {
		for i := 0; i < 10; i++ {
			fmt.Println("hello,world", i)
		}
	}
	
	func test() {
		defer func() {
			//捕获test抛出的panic
			if err := recover(); err != nil {
				fmt.Println("test()发生错误", err)
			}
		}()
		//定义一个map,没有make就赋值
		var myMap map[int]string
		myMap[0] = "golang" //报错
	}
	
	func main() {
		go sayHello()
		go test()
	
		for i := 0; i < 10; i++ {
			fmt.Println("main()ok", i)
			time.Sleep(time.Second)
		}
	}

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/995826.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存