0
点赞
收藏
分享

微信扫一扫

Golang学习(十):goroutine 和 channel

就是耍帅 2022-05-04 阅读 81
golang

文章目录

1. 需求案例

  1. 统计 1 - 9000000000 的数字中,哪些是素数?
  2. 思路
    ① 使用循环,循环判断各个数是不是素数
    ② 使用并发或并行,将统计任务分配给多个 goroutine 去完成

2. goroutine基本介绍

2.1 程序、进程和线程

  1. 进程是程序再操作系统中的一次执行
  2. 线程是进程的一个执行实例,最小基本单元
  3. 一个进程可以创建销毁多个线程,可以并发执行多个线程
  4. 一个程序至少一个进程,一个进程至少一个线程
    在这里插入图片描述

2.2 并发和并行

  1. 多线程程序在单核上运行,并发
  2. 多线程程序在多核上运行,并行
    在这里插入图片描述
    在这里插入图片描述

2.3 Go 协程和 Go 主线程

  1. Go主线程(类似传统进程):一个Go线程上,可以起多个协程,协程是轻量级的线程(编译器优化)
  2. Go协程特点
    ① 独立栈空间
    ② 共享程序堆空间
    ③ 调度由用户控制
    ④ 协程是轻量级线程
  3. 示意图
    在这里插入图片描述

3. goroutine 快速入门

3.1 案例

  1. 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 “hello,world”
  2. 在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
  3. 要求主线程和goroutine同时执行
  4. 画出主线程和协程执行流程图
package main
import (
	"fmt"
	"strconv"
	"time"
)

// 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world"
// 在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
// 要求主线程和goroutine同时执行

//编写一个函数,每隔1秒输出 "hello,world"
func test() {
	for i := 1; i <= 10; i++ {
		fmt.Println("tesst () 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.2 小结

  1. 主线程物理线程,作用在cpu上,重量级,非常耗cpu资源
  2. 协程从主线程开启,是轻量级线程,逻辑态,资源消耗小
  3. Golang协程机制是重要特点,可以轻松开启上万协程。其他编程语言一般基于线程,开启过多线程,资源消耗大。

4. goroutine 调度模型 MPG

  1. MPG 模式
    在这里插入图片描述
  2. MPG 运行状态1
    在这里插入图片描述
  3. MOG 运行状态2
    在这里插入图片描述

5. 设置Golang运行CPU数

package main
import (
	"runtime"
	"fmt"
)

func main() {
	cpuNum := runtime.NumCPU()
	fmt.Println("cpuNum=", cpuNum)

	//可以自己设置使用多个cpu
	runtime.GOMAXPROCS(cpuNum - 1)
	fmt.Println("ok")
}

6. channel(管道)

6.1 需求

  1. 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成
  2. 思路
    ① 编写一个函数,来计算各个数的阶乘,并放入到 map中.
    ② 我们启动的协程多个,统计的将结果放入到 map中
    ③ map 应该做出一个全局的.

不加锁的情况
问题一:主程序跑完了,协程还没跑完,结束了
问题二:所有协程同时操作map

先build后生成exe再运行,会报错显示有八个数据产生竞争关系,因为系统八核

package main
import (
	"fmt"
	_ "time"
	"sync"
)

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

// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.

var (
	myMap = make(map[int]int, 10)  
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
	
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}

	//这里我们将 res 放入到myMap
	myMap[n] = res //concurrent map writes?
}

func main() {

	// 我们这里开启多个协程完成这个任务[200个]
	for i := 1; i <= 20; i++ {
		go test(i)
	}


	//休眠10秒钟【第二个问题,为了等一下。报错】
	//time.Sleep(time.Second * 5)

	//这里我们输出结果,变量这个结果
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}

}

在这里插入图片描述

6.2 使用全局变量加锁改进

在这里插入图片描述

package main
import (
	"fmt"
	_ "time"
	"sync"
)

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

// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.

var (
	myMap = make(map[int]int, 10)  
	//声明一个全局的互斥锁
	//lock 是一个全局的互斥锁, 
	//sync 是包: synchornized 同步
	//Mutex : 是互斥
	lock sync.Mutex
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
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 <= 20; 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()

}

在这里插入图片描述
在这里插入图片描述

6.3 为什么需要channel

  1. 主线程等待所有goroutine全部完成时间很难确定,设置10秒只是估算。短了还有go没完成,长了浪费时间
  2. 全局变量加锁同步实现通讯,不利于多个协程对全局变量读写操作

6.4 channel 基本介绍

  1. 本质是队列,FIFO
  2. 不需要加锁,本身线程安全
  3. 有类型限制,一个string channel 只能放string

在这里插入图片描述

6.5 声明/定义 channel

在这里插入图片描述

6.6 channel 初始化,写入,读取

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
	// //如果从channel取出数据后,可以继续放入
	<-intChan
	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.Println("num2=", num2)
	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*/)

}

6.7 channel 关闭、遍历

  1. 关闭后不能再写数据,但是可以读取
  2. channel 支持 for-range 遍历,如果没有关闭就遍历显示 deadback
package main
import (
	"fmt"
)

func main() {

	intChan := make(chan int, 3)
	intChan<- 100
	intChan<- 200
	close(intChan) // close
	//这是不能够再写入数到channel
	//intChan<- 300
	fmt.Println("okook~")
	//当管道关闭后,读取数据是可以的
	n1 := <-intChan
	fmt.Println("n1=", n1)


	//遍历管道
	intChan2 := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan2<- i * 2  //放入100个数据到管道
	}

	//遍历管道不能使用普通的 for 循环
	// for i := 0; i < len(intChan2); i++ {

	// }
	//在遍历时,如果channel没有关闭,则会出现deadlock的错误
	//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
	close(intChan2)
	for v := range intChan2 {
		fmt.Println("v=", v)
	}


}

6.8 应用实例1

在这里插入图片描述
在这里插入图片描述

package main
import (
	"fmt"
	"time"
)


//write Data
func writeData(intChan chan int) {
	for i := 1; i <= 50; i++ {
		//放入数据
		intChan<- i //
		fmt.Println("writeData ", i)
		//time.Sleep(time.Second)
	}
	close(intChan) //关闭
}

//read data
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
	close(exitChan)

}

func main() {

	//创建两个管道
	intChan := make(chan int, 10)
	exitChan := make(chan bool, 1)
	
	go writeData(intChan)
	go readData(intChan, exitChan)

	//time.Sleep(time.Second * 10)
	for {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}

}

6.9 应用实例2 - 阻塞

在这里插入图片描述

6.10 应用实例3 - 最开始的素数问题

统计1 - 200000 中,那些是素数
在这里插入图片描述

package main
import (
	"fmt"
	"time"
)



//向 intChan放入 1-8000个数
func putNum(intChan chan int) {

	for i := 1; i <= 80000; i++ {    
		intChan<- i
	}

	//关闭intChan
	close(intChan)
}

// 从 intChan取出数据,并判断是否为素数,如果是,就
// 	//放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {

	//使用for 循环
	// var num int
	var flag bool // 
	for {
		//time.Sleep(time.Millisecond * 10)
		num, ok := <-intChan //intChan 取不到..
		
		if !ok { 
			break
		}
		flag = true //假设是素数
		//判断num是不是素数
		for i := 2; i < num; i++ {
			if num % i == 0 {//说明该num不是素数
				flag = false
				break
			}
		}

		if flag {
			//将这个数就放入到primeChan
			primeChan<- num
		}
	}

	fmt.Println("有一个primeNum 协程因为取不到数据,退出")
	//这里我们还不能关闭 primeChan
	//向 exitChan 写入true
	exitChan<- true	

}

func main() {

	intChan := make(chan int , 1000)
	primeChan := make(chan int, 20000)//放入结果
	//标识退出的管道
	exitChan := make(chan bool, 8) // 4个



	start := time.Now().Unix()
	
	//开启一个协程,向 intChan放入 1-8000个数
	go putNum(intChan)
	//开启4个协程,从 intChan取出数据,并判断是否为素数,如果是,就
	//放入到primeChan
	for i := 0; i < 8; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}

	//这里我们主线程,进行处理
	//直接
	go func(){
		for i := 0; i < 8; i++ {
			<-exitChan
		}

		end := time.Now().Unix()
		fmt.Println("使用协程耗时=", end - start)

		//当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
		close(primeChan)
	}()


	//遍历我们的 primeChan ,把结果取出
	for {
		_, ok := <-primeChan
		if !ok{
			break
		}
		//将结果输出
		//fmt.Printf("素数=%d\n", res)
	}

	fmt.Println("main线程退出")


	
}

在这里插入图片描述
传统for循环方法

package main
import (
	"time"
	"fmt"
)

func main() {

		start := time.Now().Unix()
		for num := 1; num <= 80000; num++ {

			flag := true //假设是素数
			//判断num是不是素数
			for i := 2; i < num; i++ {
				if num % i == 0 {//说明该num不是素数
					flag = false
					break
				}
			}

			if flag {
				//将这个数就放入到primeChan
				//primeChan<- num
			}

		}
		end := time.Now().Unix()
		fmt.Println("普通的方法耗时=", end - start)
		
}

在这里插入图片描述

6.11 channel 细节

  1. channel 可以声明为只读或只写。默认是双向
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 //error

	fmt.Println("chan2=", chan2)

	//3. 声明为只读
	var chan3 <-chan int
	num2 := <-chan3
	//chan3<- 30 //err
	fmt.Println("num2", num2)

}

实际应用如下,我们希望send只写,recv只读
在这里插入图片描述
2. select 解决从管道取数据阻塞问题

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 {
			//注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
			//,会自动到下一个case匹配
			case v := <-intChan : 
				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.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
				time.Sleep(time.Second)
				return 
				//break label
		}
	}
}
  1. goroutine 中使用 recover,解决协程中出现panic,导致程序崩溃问题
    在这里插入图片描述
package main
import (
	"fmt"
	"time"
)

//函数
func sayHello() {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		fmt.Println("hello,world")
	}
}
//函数
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" //error
}

func main() {

	go sayHello()
	go test()


	for i := 0; i < 10; i++ {
		fmt.Println("main() ok=", i)
		time.Sleep(time.Second)
	}

}
举报

相关推荐

0 条评论