golang goroutine协程 和 channel管道

前言

进程和线程基本介绍

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

程序,进程和线程的关系图

在这里插入图片描述

并发和并行

  1. 多线程合成在单核上运行,就是并发
    在这里插入图片描述

  2. 多线程乘车在多核上运行,就是并行
    在这里插入图片描述
    总结:
    并发:
    因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这个10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
    并行:
    因为是在多个cpu上(比如有10个cpu),比如有10个进程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这个10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

Go协程和Go主线程

  1. Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,你可以这样理解,协程就是轻量级的线程
  2. Go协程的特点
    1. 有独立的栈空间
    2. 共享程序堆空间
    3. 调度由用户控制
    4. 协程是轻量级的协程
      在这里插入图片描述

goroutine 快速入门

案例一

package main

import(
	"fmt"
	"time"
	"strconv"
)

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

//编写一个函数,每个一秒输出 ”hello,world“
func test(){
	for i := 1; i <= 10; i++ {
		fmt.Println("test() hello,world " + strconv.Itoa(i))
		time.Sleep(time.Second) //宕机一秒
	}
}

func main(){

	//普通方式
	// test()
	/*
	结果
	test() hello,world 1
	test() hello,world 2
	....
	test() hello,world 10
	main() hello,golang 1
	main() hello,golang 2
	....
	main() hello,golang 10
	普通方式只有test()执行完成后才会执行主进程for
	*/

	go test()//开启一个协程
	/*
	main() hello,golang 1
	test() hello,world 1
	test() hello,world 2
	main() hello,golang 2
	main() hello,golang 3
	test() hello,world 3
	....
	test() hello,world 10
	main() hello,golang 10
    开启协程后会同时执行 
	*/

	for i := 1; i <= 10; i++ {
		fmt.Println("main() hello,golang " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

在这里插入图片描述

小结:

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

MPG模式基本介绍

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

设置Golang 运行的CPU

package main

import(
	"fmt"
	"runtime"
)

func  main()  {
	//NumCPU返回本地机器的逻辑CPU个数
	cpuNum := runtime.NumCPU()

	//可以自己设置使用多个CPU
	runtime.GOMAXPROCS(cpuNum - 1)
	fmt.Printf("cpuNum=%v",cpuNum)
	//结果cpuNum=12
}

channel(管道) 快速入门

案例

package main

import(
	"fmt"
)

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

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

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

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

  //将结果放入map中
  myMap[n] = res
}

func main(){
   //开启协程 开启200个协程完成这个任务
   for i := 1; i <= 200; i++ {
	   go test(i)
   }

   //输出结果
   for k, v := range(myMap){
	   fmt.Printf("myMap[%d] = %d\n",k,v)
   }
   /*
    错误:
    fatal error: concurrent map writes
	fatal error: concurrent map iteration and map write
	fatal error: concurrent map writes
	错误为 并发映射输入
   */
}

在编译该程序时,郑家一个参数 -race 在运行时也会提示一些错误 看演示

go build -race .\main.go
.\main.exe
错误信息
myMap[140] = 0
myMap[32] = -6045878379276664832
myMap[82] = 0
myMap[127] = 0
myMap[149] = 0
Found 2 data race(s)
//也会提示 有2个数据产生并发写入问题

在这里插入图片描述

解决方法

  1. 全局变量加锁同步
  2. channel

全局变量加锁同步

package main

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

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

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

  //将结果放入map中
  //加锁
  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 k, v := range(myMap){
	   fmt.Printf("myMap[%d] = %d\n",k,v)
   }
  //解锁
  lock.Unlock()
  /*
  结果:
    myMap[29] = -7055958792655077376
	myMap[37] = 1096907932701818880
	myMap[44] = 2673996885588443136
	myMap[50] = -3258495067890909184
	myMap[61] = 3098476543630901248
	myMap[123] = 0
	myMap[136] = 0
	myMap[139] = 0
	myMap[143] = 0
	结果成功 没有提示并发插入数据
  */

}

channel基本介绍

为什么需要channel
前面使用全局变量加锁同步解决goroutine的通讯,但不完美

  1. 主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置了10秒,仅仅是估算
  2. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随着主线程的退出而销毁
  3. 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作
  4. 上面种种分析都在呼唤一个新的通讯机制 channel

channel的介绍

  1. channel本质就是一个数据结构-队列 如下图
    在这里插入图片描述
  2. 数据是先进先出
  3. 线程安全,多个goroutine访问时,不需要加锁
  4. channel是有类型的,一个string的channel只能存放string类型数据

channel基本使用

  1. var 变量名 chan 数据类型
  2. var intChan chan int (intChan用于存放int数据)
  3. var mapChan chan map[int]string(mapChan用于存放map[int]string类型)
  4. var perChan chan Person
  5. var perChan2 chan *Person

说明

  1. channel是引用类型
  2. channel必须初始化才能写入数据,即make后才能使用
  3. 管道是由类型的,intChan只能输入整数int
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)
	//结果 intChan 的值=0xc00001e100 intChan的地址值=0xc000006028


	//3.向管道写入数据
	intChan <- 10
	num := 20
	intChan <- num
	intChan <- 50
	// intChan <- 60 //写入管道的数据,不可以超过其容量 否则会报错 deadlock!
	fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))  // 3,3
	

    //4. 从管道中读取数据
	var num2 int
	num2 = <- intChan
	fmt.Println(num2)//结果 10
	// 读取时同理不可以超过存储容量 否则会报错 deadlock!
	fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))  // 2,3


	//5.当chan 存储满后 需要读取出一条数据后 才可以再次存储
	intChan  <- 60
	fmt.Printf("channel len=%v cap=%v \n",len(intChan),cap(intChan))  // 3,3
}

channel 演示案例一

package main

import(
  "fmt"
)

type Cat struct{
	Name string
	age int
}

func main()  {
	//定义一个存放任何数据类型的管道 3个数据
	/*
	var allChan chan interface{}
	allChan = make(chan interface{}, 3)
	*/
	allChan := make(chan interface{}, 3)//和上面命名同样效果

	allChan <- 10
	allChan <- "tom"
	cat := Cat{"喵喵",12}
	allChan <- cat

	//希望获取第三个元素,则先将前俩个推出
	<- allChan
	<- allChan

	newCat := <- allChan
	fmt.Printf("newCat=%T newCat=%v\n",newCat,newCat)
	//结果 :newCat=main.Cat newCat={喵喵 12}

    //下面获取newCat.Name 是无法获取到名称的 ,需要进行断言
	// fmt.Printf("newCat=%T newCat=%v\n",newCat,newCat.Name)


	newCatInfo := newCat.(Cat) 
	fmt.Printf("newCat=%T newCat=%v\n",newCatInfo,newCatInfo.Name)
	/*
	 结果
	 newCat=main.Cat newCat=喵喵
	*/
}

channel的遍历和关闭

channel关闭

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

package main

import(
	"fmt"
)

func main(){
	intChan := make(chan int ,3)
	intChan <- 10
	intChan <- 20
	close(intChan)//关闭后无法写入
	//intChan <- 30 //提示:panic: send on closed channel
	fmt.Println("ok~!")

	//可以正常获取管道中的值
	n1 := <- intChan
	fmt.Println(n1)
	//结果 10
}

channel的遍历

package main

import(
	"fmt"
)

func main(){

   //创建一个管道并且添加数据	
   intChan := make(chan int , 100)
   for i := 0; i < 100; i++ {
	 intChan <- i * 2
   }

   //在遍历时,如果channel没有关闭,则会出现all goroutines are asleep - deadlock!的错误

   //在遍历时,如果channel已经关闭,则会正常遍历数据
   close(intChan)

   //循环获取intChan数据
   for v := range intChan {
	   fmt.Printf("v=%v\n",v)
   }
}

goroutine和channel结合

应用案例一

package main

import(
	"fmt"
)

//writeData写入数据
func writeData(intChan chan int){
     for i := 1; i <= 50; i++ {
		intChan <- i
		fmt.Printf("writeData 插入的数据=%v\n",i)
	 }
	 //关闭管道
	 close(intChan)
}

//readData 读取数据
func readData(intChan chan int,exitChan chan bool){
	for {
		v,ok := <-intChan
		//关闭管道时 intChan返回ok数据是否读取完毕
		if !ok {
			break
		}
		fmt.Printf("readData 读取的数据=%v\n",v)
	}

	//读取完成后 验证改为true
	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
		}
	}
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值