Go第 16 章 :goroutine 和 channel

本文详细介绍了Go语言中的goroutine和channel,包括它们在并发和并行计算中的作用,以及如何通过channel实现goroutine间的通信和同步。通过示例代码展示了如何避免并发访问共享资源时的死锁问题,以及如何利用channel高效地传递数据。此外,还讨论了channel的使用细节和注意事项,如只读/只写channel以及select语句的应用。
摘要由CSDN通过智能技术生成

Go第 16 章 :goroutine 和 channel

16.1 goroutine-看一个需求

需求:要求统计 1-9000000000 的数字中,哪些是素数?
分析思路:

  1. 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
  2. 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到 goroutine.【速度提高 4 倍】

16.2 goroutine-基本介绍

16.2.1 进程和线程介绍

请添加图片描述

16.2.2 程序、进程和线程的关系示意图

请添加图片描述

16.2.3 并发和并行

 并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行
  3. 示意图:
    请添加图片描述
    请添加图片描述
16.2.4 Go 协程和 Go 主线程

Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以
这样理解,协程是轻量级的线程[编译器做优化]。
Go 协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由程序员控制
  4. 协程是轻量级的线程
    请添加图片描述

16.3 goroutine-快速入门

16.3.1 案例说明

请编写一个程序,完成如下功能:

  1. 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 “hello,world”
  2. 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
  3. 要求主线程和 goroutine 同时执行.
  4. 画出主线程和协程执行流程图
    请添加图片描述
    请添加图片描述
    请添加图片描述
16.3.2 快速入门小结
  1. 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一
    般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了

16.4 goroutine 的调度模型

16.4.1 MPG 模式基本介绍

请添加图片描述

16.4.2 MPG 模式运行的状态 1

请添加图片描述

16.4.3 MPG 模式运行的状态 2

请添加图片描述

16.5 设置 Golang 运行的 cpu 数

介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目

请添加图片描述

16.6 channel(管道)-看个需求

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

package main

import (
	"fmt"
	"time"
)

var (
	myMap =make(map[int]int,10)
)
// test 函数就是计算 n!, 让将这个结果放入到 myMa
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<=200;i++{
		go test(i)
	}

	//休眠 10 秒钟【第二个问题 】
	time.Sleep(time.Second*10)
	//这里我们输出结果,变量这个结果
	for i,v:=range myMap{
		fmt.Printf("map[%d]=%d\n",i,v)
	}
}

请添加图片描述

16.6.1 不同 goroutine 之间如何通讯
    1. 全局变量的互斥锁
    1. 使用管道 channel 来解决
16.6.2 使用全局变量加锁同步改进程
因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent mapwrites
解决方案:加入互斥锁
我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)

代码改进

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	myMap =make(map[int]int,10)
	//声明一个全局的互斥锁
	//lock:是一个全局的互斥锁
	//sync是包:synchornized同步
	//Mutex:是互斥
	lock sync.Mutex
)
// test 函数就是计算 n!, 让将这个结果放入到 myMa
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<=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()
}
16.6.3 为什么需要 channel

请添加图片描述

16.6.4 channel 的基本介绍

请添加图片描述

解释:main遍历map的时候有可能协程没执行完,那遍历map的时候有可能和协程同时操作map所以韩老师在这里也加锁

  1. channle 本质就是一个数据结构-队列【示意图】
  2. 数据是先进先出【FIFO : first in first out】
  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
  4. channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
  5. 示意图:
    请添加图片描述
16.6.5 定义/声明 channel
  • var 变量名 chan 数据类型
  • 举例:
    var intChan chan int (intChan 用于存放 int 数据)
    var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
    var perChan chan Person
    var perChan2 chan *Person

-说明
channel 是引用类型
channel 必须初始化才能写入数据, 即 make 后才能使用
管道是有类型的,intChan 只能写入 整数 int

16.6.6 管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
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、向g管道写入数据
	intChan <-10
	num:=21
	intChan<-num
	intChan<-50
	// intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量
	//4. 看看管道的长度和 cap(容量)
	fmt.Printf("channel len =%v cap=%v\n",len(intChan),cap(intChan))

	//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)
	fmt.Println("num3=", num3, "num4=", num4)

}

16.6.7 channel 使用的注意事项
  1. channel 中只能存放指定的数据类型
  2. channle 的数据放满后,就不能再放入了
  3. 如果从 channel 取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock
16.6.8 读写 channel 案例演示

请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

请添加图片描述

有的情况需要使用类型断言

请添加图片描述

16.7 管道的课后练习题

请添加图片描述

package main

import (
	"fmt"
	"math/rand"
	"strconv"
)

type Person struct{
	Name string
	Age int
	Address string
}

func WriteChan(PersonChan chan Person){
	for i:=0;i<10;i++{
		PersonChan <-Person{
			Name: "Person"+strconv.Itoa(rand.Intn(100)),
			Age:rand.Intn(100),
			Address:"成都市成华大道"+strconv.Itoa(rand.Intn(100))+"号",
		}
		fmt.Println("write data=",i)
	}
	close(PersonChan)
}
func ReadChan(PersonChan chan Person,exitChan chan bool){
	i:=0
	for{
		i++
		v,ok:=<-PersonChan
		if !ok{
			break
		}
		fmt.Printf("第%v个perosn数据的Name=%v,Age=%v,Address=%v\n",i,v.Name,v.Age,v.Address)
	}
	exitChan <-true
	close(exitChan)

}

func main(){


	PersonChan:=make(chan  Person,100)
	exitChan:=make(chan bool,1)
	go WriteChan(PersonChan)
	go ReadChan(PersonChan,exitChan)
	for {
		_, ok := <- exitChan
		if !ok {
			break
		}
	}
}

16.8 channel 的遍历和关闭

16.8.1 channel 的关闭

使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然 可以从该 channel 读取数据
案例演示:
请添加图片描述

16.8.2 channel 的遍历

channel 支持 for–range 的方式进行遍历,请注意两个细节 1) 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误 2) 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

16.8.3 channel 遍历和关闭的案例演示

看代码演示:
请添加图片描述

16.8.4 应用实例 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, 50)
	exitChan := make(chan bool, 1)
	go writeData(intChan) 
	go readData(intChan, exitChan)
	//time.Sleep(time.Second * 10) 
	for {
		_, ok := <-exitChan
	if !ok {
		break 
	}
}
}
16.8.5 应用实例 2-阻塞

请添加图片描述

16.8.6 应用实例 3

需求:
要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine 和 channel 的知识后,就可以完成了 [测试数据: 80000]
分析思路:
传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。
使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine去完成,完成任务时间短。
请添加图片描述

前一节老师说了,写满的时候这个协程会阻塞的,但是不会报错,因为编译器知道有协程在读,等从管道中取出数字之后就可以继续写了 intchan :=make(chan int 1000)

package main

import (
	"fmt"
	"time"
)

func putNum(intChan chan int) {
	for i := 0; i < 80000; i++ {
		intChan <- i
	}
	close(intChan)
}

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 {
			break
		}
		flag = true
		for i := 2; i < (num+1)/2; i++ {
			if num%i == 0 { //说明该 num 不是素数
				flag = false
				break
			}
		}
		if flag {
			primeChan <- num
		}
	}
	fmt.Println("有一个primeChan协程因为取不到数据,退出")
	//这里我们还不能关闭 primeChan,因为有可能别人还在处理
	//向 exitChan 写入 true
	exitChan <- true
}

func main() {
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 20000)
	//标识退出的管道
	exitChan := make(chan bool, 1024) //4个
	//开始统计时间
	start:=time.Now().Unix()


	//开启一个协程,向intChan放入1-80000个数
	go putNum(intChan)
	//开启4个协程,从intChan取出数据,并判断是否为素数,如果是,就放入到primeChan
	for i := 0; i < 1024; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}
	//这里我们主线程,进行处理
	//直接
	go func() {
		for i := 0; i < 1024; i++ {
			<-exitChan//取不到4个就等待,取到了就扔出去
		}

		//结束时间统计
		end:=time.Now().Unix()
		fmt.Printf("使用协程耗时=%ds\n",end-start)

		//当我们从exitChan取出来4个结果,就可以放心的关闭primeChan
		close(primeChan)
	}()
	//遍历我们的primeChan把结果取出
	for {
		_, ok := <-primeChan
		if !ok {
			break
		}
		//fmt.Printf("素数=%d\n", res)
	}
	fmt.Println("main线程退出")

}


结论:使用 go 协程后,执行的速度,比普通方法提高至少 4 倍

16.9 channel 使用细节和注意事项

  1. channel 可以声明为只读,或者只写性质 【案例演示】
    请添加图片描述
  2. channel 只读和只写的最佳实践案例
    请添加图片描述
  3. 使用 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个数据stirng
	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:
			fmt.Printf("从intChan读取的数据%d\n", v)
			time.Sleep(time.Second)
		case v := <-stringChan:
			fmt.Printf("从stringChan读取的数据%d\n", v)
			time.Sleep(time.Second)
		default:
			fmt.Printf("都取不到了,不玩了,程序员可以假如逻辑\n")
			time.Sleep(time.Second)
			return
			//break label
		}
	}
}
select简介
    1、Go的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前groutine。

    2、select是Golang在语言层面提供的I/O多路复用的机制,其专门用来检测多个channel是否准备完毕:可读或可写。

    3、select语句中除default外,每个case操作一个channel,要么读要么写

    4、select语句中除default外,各case执行顺序是随机的

    5、select语句中如果没有default语句,则会阻塞等待任一case

    6、select语句中读操作要判断是否成功读取,关闭的channel也可以读取
  1. goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
    请添加图片描述
在Golang中,if 后面可以紧着 并列 写出变量的定义,使用分号 “;” 和 if的表达式隔开。
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 {//在Golang中,if 后面可以紧着 并列 写出变量的定义,使用分号 “;” 和  if的表达式隔开。
			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)
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值