Go_协程与管道

本文介绍了Go语言中的协程和通道特性。协程是轻量级线程,由Goruntime管理,具有独立栈空间,调度灵活。通道作为协程间通信的手段,保证了线程安全,避免了资源竞争。文章通过实例展示了如何创建和使用协程以及通道,以及通道的关闭和遍历,强调了Go语言中并发编程的高效性和易用性。
摘要由CSDN通过智能技术生成

协程(goroutine)

问题引入

基本介绍

  • 进程和线程的关系;
    • 进程是系统进行资源分配和调度的基本单位
    • 一个进程可以有多个线程
    • 同一个进程的多个线程并发的执行
  • 并发和并行
    • 多线程程序在单核上进行,就是并发
    • 多线程程序在多核上运行,就是并行
      图例说明

Go协程和Go主线程

  • Go主线程(有的程序员直接称为 线程/也可理解成进程):一个Go线程上,可以起多个协程,协程是轻量级的线程【编译器做了优化】
  • Go协程的特点:
    • 有独立的栈空间
    • 共享程序堆空间
    • 调度由用户控制
    • 协程是轻量级的线程
      在这里插入图片描述

协程快速入门

  • 案例:请编写一个程序,完成如下功能
    • 在主线程中,开启一个goroutine,该协程每隔1秒输出“hello,world”
    • 在主线程中也每隔一秒输出“hello,golang”,输出10次后,退出程序
    • 要求主线程和goroutine同时执行
    • 画出主线程和协程执行流程图
  • 代码和运行结果
    在这里插入图片描述
  • 执行流程图
    在这里插入图片描述
  • 小结:
    • 主线程是一个物理线程,直接作用在CPU上的,是重量级的,非常耗费CPU资源。
    • 协程从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小
    • Golang的协程机制是重要的特点,可以轻松开启上万个协程。其他编程语言的并发机制一般是基于线程的,开启过多线程,资源耗费大

goroutine 调度模型(MPG)

  • M:操作系统的主线程(是物理线程)
  • P:协程执行需要的上下文环境,是实现从N:1到N:M映射的关键
  • G:协程
    在这里插入图片描述
    在这里插入图片描述

runtime包

设置Golang运行的CPU数

  • 运用到的runtime包中的方法
  • func NumCPU() int:返回本地机器的逻辑CPU个数
  • func GOMAXPROCS(n int) int:设置可同时执行的最大CPU数,并返回先前的设置。
  • go1.8后,默认让程序运行在多个核上,可以不用设置;之前的版本,可以设置一下,可让CPU利用率更高效
package main
import (
	"fmt"
	"runtime"
)

func main(){
	//获取当前系统CPU的数量
	cpuNum := runtime.NumCPU()
	fmt.Println("cpuNum=", cpuNum)
	//可以自己设置使用多少个CPU
	runtime.GOMAXPROCS(cpuNum - 1)
	fmt.Println("ok")
}

管道(channel)

引入

  • 需求:现要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成
  • 代码实现与问题发现:
    在这里插入图片描述
  • 上述代码,需求中,存在多个协程向同一块map空间中写数据的操作
  • 资源竞争命令:1)go build -race main.go;2)main.exe

解决方法

不同goroutine之间如何通信:

1)全局变量的互斥锁

在这里插入图片描述
使用了sync包:sync包提供了基本的同步基元,如互斥锁。大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。
代码改进:
在这里插入图片描述
缺点:

  • 主线程在等待所有goroutine全部完成时间是很难确定的
  • 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作

-2) 管道(channel)

在这里插入图片描述

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

channel的定义/声明

1. 定义/声明 channel
在这里插入图片描述

  • 代码示例:
package main
import (
	"fmt"
)

func main(){
	//演示管道的使用
	//创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int, 3)
	//看看channel是什么
	fmt.Printf("intChan的值=%v  intChan本身的地址=%p\n", intChan, &intChan)

	//向管道写入数据
	intChan<- 10
	num := 211
	intChan<- num

	//看看管道的长度和容量;管道的容量分配内存时已经确定,不会动态增长
	fmt.Printf("intChan的长度=%v   容量=%v \n", len(intChan), cap(intChan))

	//从管道中取数据
	var num2 int
	num2 = <-intChan
	fmt.Println("num2=", num2)

	//观察管道的长度,容量
	fmt.Printf("intChan的长度=%v   容量=%v \n", len(intChan), cap(intChan))
	//在没有使用协程的情况下,若管道中的数据已经全部取出,再取就会报告 deadlock
}
  • 运行结果:
    在这里插入图片描述
    2. 案例演示
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

注意

在这里插入图片描述

  • 上图划红线的地方,编译不通过;
  • 纠正代码如下:
    在这里插入图片描述

channel的遍历与关闭

1. channel关闭:使用内置函数close

package main
import (
	"fmt"
)
func main(){
	intChan := make(chan int, 3)
	intChan<- 10
	intChan<- 20
	//关闭channel
	close(intChan)
	//此时不能在写入数据,但是可以读取
	num := <- intChan
	fmt.Println("num", num)
}

2. channel遍历:使用for-range方式

  • 在遍历时,如果channel没有关闭,则会出现deadlock的错误
  • 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
package main
import (
	"fmt"
)
func main(){
	intChan := make(chan int, 3)
	intChan<- 10
	intChan<- 20
	//关闭channel
	close(intChan)
	//此时不能在写入数据,但是可以读取
	num := <- intChan
	fmt.Println("num", num)

	//遍历管道
	intChan2 := make(chan int, 20)
	for i := 0; i < 20; i++{
		intChan2<- i*2   //放入20个数据到channel中
	}
	//遍历管道不能使用普通for循环
	/*在遍历时,没有关闭管道,会遍历输出,
			但会出现fatal error: all goroutines are asleep - deadlock!*/
	close(intChan2)
	for v := range intChan2{
		fmt.Println("v=", v)
	}
}

goroutine 和channel结合

在这里插入图片描述
思路分析:
在这里插入图片描述
代码实现:

package main
import (
	"fmt"
)
func WriteData(intChan chan int){
	for i := 1; i <= 50; i++{
		intChan<- i
		fmt.Println("写入数据", i)
	}
	close(intChan)
}

func ReadData(intChan chan int, exitChan chan bool){
	for{
		v, ok := <-intChan
		if !ok{
			break
		}
		fmt.Println("读取的数据", v)
	}
	exitChan<- true
	close(exitChan)
}

func main(){
	//创建两个协程
	intChan := make(chan int, 50)
	exitChan := make(chan bool, 1)
	
	go WriteData(intChan)
	go ReadData(intChan, exitChan)
	for{
		_, ok := <-exitChan
		if !ok{
			break
		}
	}
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码实现:

package main
import (
	"fmt"
)
func putNum(intChan chan int){
	for i := 1; i <= 8000; i++{
		intChan<-i
	}
	close(intChan)
}
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool){
	for{
		num, ok := <-intChan
		if !ok{
			break
		}
		var flag = true //假定为素数
		//判断num是否为素数
		for i := 2; i < num; i++{
			if num % i == 0{
				flag = false
				break
			}
		}
		if flag{
			primeChan<- num
		}
	}
	exitChan<- true
	fmt.Println("有一个协程已经走完了")
}
//统计1-8000中的素数个数
func main(){
	//声明管道
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 4000) //放素数结果
	exitChan := make(chan bool, 4) //标识退出的管道
	//开启一个协程putNum,向 intChan中放1-8000个数
	start := time.Now().Unix()
	go putNum(intChan)
	//开启4个协程primeNum,从intChan中取出数据,并判断是否为素数
	//如果是,就将其放入primeChan管道中
	for i := 0; i < 4; i++{
		go primeNum(intChan, primeChan, exitChan)
	}
	//另开起一个协程,用于判断是否上述4个primeNum协程已经运行完
	go func(){
		for i := 0; i < 4; i++{
			<-exitChan
		}
		end := time.Now().Unix()
		fmt.Println("使用协程耗时=", end - start)
		//当从exitChan中取出4个值后,说明primeNum协程已经全部结束
		//此时可以关闭primeChan管道
		close(primeChan)
	}()
	//遍历取出结果
	for{
		res, ok := <-primeChan
		if !ok{
			break
		}

		//将结果输出
		fmt.Println("是素数的有:", res)
	}

}

channel使用注意事项

  • channel可以声明为只读,或者只写性质
package main
imoprt (
	"fmt"
)
func main(){
	//管道可以只声明为只读 或者 只写

	var intChan chan int //双向管道,可读可写
	var intChan2 chan<- int //只能写
	var intChan3 <-chan int //只能读
	intChan2 = make(chan int, 2)
	intChan3 = make(chan int, 5)
}
  • 使用select可以解决从管道取数据的阻塞问题
package main
import (
	"fmt"
)
func main(){
	//使用select可以解决从管道取数据的阻塞问题
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++{
		intChan<-i
	}
	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)
		case v := <-stringChan:
			fmt.Printf("从stringChan读取的数据%s\n", v)
		default:
			fmt.Printf("都取不到数据\n")
			return
			//break label
		}
	}
}
  • goroutine中使用recover,解决协程中出现panic,导致程序崩溃的问题
    • 说明:若起了一个协程,但是协程出现了panic;若没有捕获这个panic,就会造成整个程序崩溃。这时我们可以在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"  //此处发生错误
}

func main(){
	go sayHello()
	go test()
	time.Sleep(time.Second * 20)
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值