Go的goroutine和channel

Go的goroutine和channel

goroutine

  • 进程和线程说明
    • 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
    • 线程是进程的一个执行实例,是程序执行的最小单元,是比进程更小的能独立运行的基本单位
    • 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
    • 一个程序至少有一个进程,一个进程至少有一个线程
  • 并发和并行
    • 并发:多线程程序在单核上运行
      • 因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度上看,好像这10个线程都在运行,但是从微观上看,在某个时间点看,其实只有一个线程在执行
    • 并行:多线程程序在多核上运行
      • 因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度上看,这10个线程都在运行,从微观上看,在某个时间点,也同时有10个线程在执行
  • Go协程和Go主线程
    • Go主线程(直接称为线程或进程):一个Go线程上,可以其多个协程。协程是轻量级的线程(编译器做优化)
    • Go协程的特点
      • 有独立的栈空间
      • 共享程序堆空间
      • 调度由用户控制
      • 协程是轻量级的线程
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)
	}
}

//如果主线程退出了,协程还没有执行完毕,也会退出
  • 主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源
  • 协程是从主线程开启的,是轻量级的,是逻辑态,对资源消耗相对小
  • Golang的协程机制是重要的特点,可以轻松开启上万哥协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程资源耗费大
  • goroutine的调度模型MPG模式
    • M:操作系统的主线程
    • P:协程执行需要的上下文
    • G:协程
    • 若M0主线程正在执行GO协程,另外有三个协程在队列等待,如果GO协程阻塞比如读取文件或数据库等,就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的GO仍然指向文件io的读写,等到GO不阻塞的了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时GO又会被唤醒
  • Go设置运行cpu数目
package main

import (
	"fmt"
	"runtime"
)

func main(){
	cpuNum := runtime.NumCPU()
	fmt.Println("numCPU=",cpuNum)
	//返回逻辑CPU个数
	
	//可以设置使用多个cpu
	runtime.GOMAXPROCS(cpuNum-1)
	fmt.Println("ok")
}

提前退出go程
  • return:退出当前函数
  • exit:退出当前进程
  • GOEXIT:提前退出当前go程

管道

协程并发(并行)资源竞争问题
  • 在编译程序时,增加参数**-race**可以知道是否存在资源竞争问题
  • 不同goroutine之间如何通讯
    • 全局变量的互斥锁
    • 使用管道channel解决
package main

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

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

func test(n int) {
	res := 1
	for i := 1;i <= n;i++ {
		res *= i
	}

	//加锁
	lock.Lock()
	myMap[n] = res
	//解锁
	lock.Unlock()
}

func main(){
	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()
}

  • channel管道介绍
    • channel本质是一个数据结构——队列
    • 数据是先进先出
    • 线程安全,多goroutine访问时,不需要加锁,
    • channel是有类型的,一个string的channel只能存放string类型数据
定义/声明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
管道的初始化,写入数据到管道,从管道读取数据
package main

import (
	"fmt"
)

func main(){
	//创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int,3)
	
	fmt.Printf("intChan的值:%v intChan本身的地址=%p\n",intChan,&intChan)
	
	//向管道写入数据
	intChan<-10
	num := 20
	intChan<-num
	intChan<-50
	//intChan<-98向管道写入数据时不能超过其容量
	
	//查看管道的长度和容量
	fmt.Printf("channel len=%v cap=%v\n",len(intChan),cap(intChan))//3,3
	
	//从管道读取数据
	var num2 int
	num2 = <-intChan
	fmt.Println("num2=",num2)
	fmt.Printf("channel len=%v channel cap=%v\n",len(intChan),cap(intChan))//2,3
	
	//在没有使用协程的情况下,如果管道数据被全部取出,再取就会报告deadlock
	num3 := <-intChan
	num4 := <-intChan
	fmt.Println("num3=",num3,"num4=",num4)
	
}
package main

import (
	"fmt"
)

type Cat struct {
	Name string
	Age int
}

func main(){
	//定义一个存放任何数据类型的管道 3个数据
	allChan := make(chan interface{},3)
	
	allChan<- 10
	allChan<-"tom jack"
	cat := Cat {"小花猫",4}
	allChan<-cat
	
	//将前两个推出
	<-allChan
	<-allChan
	newcat := <-allChan
	fmt.Printf("newcat=%T newCat=%v\n",newcat,newcat)
	//fmt.Printf("newCat.Name=%v",newCat.Name)
	//编译不给过,使用类型断言
	a := newcat.(Cat)
	fmt.Printf("newCat.Name=%v",a.Name)
}
管道的关闭和遍历
package main

import (
	"fmt"
)

//channel的关闭可以使用内置函数close,当channel关闭后,就不能再像channel写数据了,但是可以从channel读取数据
func main(){
	intChan := make(chan int,3)
	intChan<-10
	intChan<-20
	close(intChan)
	//intChan<-300
	n1 := <-intChan
	fmt.Println("n1="n1)
}
package main

import "fmt"

func main(){
	//遍历管道
	intChan := make (chan int,100)
	for i := 0;i < 100;i++ {
		intChan<-i*2
	}
	
	//遍历管道不能使用普通的for循环
	//在遍历时,如果channel没有关闭,会出现deadlock的错误
	//在遍历时,如果channel已经关闭,会正常遍历,遍历完后会退出遍历
	close(intChan)
	for v := range intChan {
		fmt.Println("v=\n",v)
	}
}
channel使用细节和注意事项
  • 如果管道只有写没有读,那么该管道会阻塞,channel可以声明为只读或只写
package main

import "fmt"

func main(){
	//在默认情况下,管道时双向的
	//var chan1 chan int可读可写

	//声明为只写
	var chan2 chan <- int
	chan2 = make (chan int,3)
	chan2<-10
	//num := <-chan2 error
	fmt.Println("chan2=",chan2)

	//声明为只读
	var chan3 <-chan int
	num2 := <-chan3
	//chan3<-10 error
	fmt.Println("num2",num2)
}
  • 使用select解决从管道取数据的阻塞问题
package main

import (
	"fmt"
	"time"
)

func main(){
	//定义一个管道10个数据int
	intChan := make(chan int,10)
	for i := 0;i < 10;i++ {
		intChan<- i
	}
	
	//定义一个管道5个数据string
	stringChan := make(chan string,5)
	for i := 0;i < 5;i++ {
		stringChan<-"hello"+fmt.Sprintf("%d",i)
	}
	
	//传统的方法在遍历管道时如果不关闭会阻塞导致deadlock
	//在实际开发中可能不好确定什么时候关闭管道
	//可以使用select方法解决
	
	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 
		}
	}
}

  • 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"//error
}

func main(){
	go sayHello()
	go test()
	
	for i := 0;i < 10;i++ {
		fmt.Println("main()ok=",i)
		time.Sleep(time.Second)
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值