go-goroutine channel

go-goroutine channel

goroutine 协程

基本介绍

  1. 进程是程序在操作系统的一次执行过程,是系统进行资源分配和调度的基本单位
  2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
  3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
  4. 一个程序至少有一个进程,一个进程至少有一个线程
  5. 并发和并行:
    1. 多线程程序在单核上运行,就是并发
    2. 多线程程序在多核上运行,就是并行
    3. 并发:因为是在一个cpu上,比如有10个线程,每个线程执行10ms,从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
    4. 并行:因为是在多个cpu上,比如10核,10个线程,每个线程执行10ms,从人的角度看,这10个线程都在运行,但是在微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
  6. go携程和go主线程
    1. go主线程(有程序员直接称为线程/也可以理解为进程):一个go线程上,可以起多个协程,协程是轻量级的线程【编译器做优化 】
    2. go协程的特点:
      1. 有独立的栈空间
      2. 共享程序堆空间
      3. 调度由用户控制
      4. 协程是轻量级的线程
func test() {
	for i := 0; i < 10; i++ {
		fmt.Println("test hello world" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

func main() { // 串行  -- 主线程
	go test() // 开启协程 : 当主线程结束后,协程自动结束

	for i := 0; i < 5; i++ {
		fmt.Println("main hello golang")
		time.Sleep(time.Second)
	}
}
  1. 主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
  3. golang的协程机制是重要的特点,可轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这就凸显了golang在并发上的优势

image-20220507171132530

goroutine的调度模型-MPG

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

image-20220507163225030

image-20220507163456016

  1. 为了充分利用多CPU的优势,在golang程序中,设置运行的cpu数目

    cpuNum := runtime.NumCPU()

  2. go1.8后,默认让程序运行在多个核上,可以不用设置

  3. Go1.8前,还要设置一下,可以更高效的利用cpu

    runtime.GPMAXPROCS(num)

channel 管道

需求

  1. 当存在资源竞争时,我们需要引入管道
  2. image-20220507172131631
import (
	"fmt"
	"time"
)

var MyMap map[int]int = make(map[int]int)

func CalN(n int) {
	res := 1
	for i := 2; i <= n; i++ {
		res *= i
	}
	MyMap[n] = res
}

func main() {

	for i := 1; i <= 200; i++ {
		go CalN(i)
	}
	time.Sleep(time.Second * 3)
	for index, val := range MyMap {
		fmt.Printf("Map[%v] = %v\n", index, val)
	}

}

加锁

var (
	MyMap map[int]int = make(map[int]int)
	// 声明一个全局的互斥锁
	// lock 是一个全局的互斥锁
	// sync synchronize 同步      mutex 互斥
	lock sync.Mutex
)

func CalN(n int) {
	res := 1
	for i := 2; i <= n; i++ {
		res *= i
	}
	// 加锁
	lock.Lock()
	MyMap[n] = res // 会产生资源竞争
	lock.Unlock()
}

channel 管道

  1. 加锁的问题:
    1. 主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置为3s,仅仅是估算
    2. 若主线程休眠时间过长,会增加等待时间,若休眠时间短,则会使某些goroutine未完成操作就被销毁了
    3. 通过全局变量加锁同步来实现通讯,也并不利用多个goroutine对全局变量的读写操作

介绍

  1. channel本质就是一个数据结构-队列
  2. 数据是先进先出
  3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
  4. channel是有类型的,一个string的channel只能存放string类型

基本使用

  1. 定义和声明var 变量名 chan 数据类型
var intChan chan int
var stringChan chan string
var sliceChan chan []float64
var mapChan chan map[int]string
var personChan chan Person
var personChan2 chan *Person
  1. channel是引用类型
  2. channel必须初始化才能写入数据,即make后才能使用
  3. 管道是有类型的,intChan只能写入整数 int
func main() {
	// 1. 定义或声明  make空间
	var intChan chan int = make(chan int, 3)
	// 2. 引用类型
	fmt.Printf("intChan: %v\n", intChan) // intChan: 0xc0000260c0
	// 3. 向管道写入数据
	// 若存入数据时,超出了cap,则会报deadlock
	intChan <- 20
	var num int = 211
	intChan <- num
	// 4. len cap
	fmt.Printf("len(intChan): %v\n", len(intChan)) // len(intChan): 2
	fmt.Printf("cap(intChan): %v\n", cap(intChan)) // cap(intChan): 3
	// 5. 读取数据
	// 在没有使用goroutine的情况下,若我们的channel数据已经被全部取出,再去就会报告deadlock死锁
	var num2 int = <-intChan
	fmt.Printf("num2: %v\n", num2)                 // num2: 20
	fmt.Printf("len(intChan): %v\n", len(intChan)) // len(intChan): 1
	num3 := <-intChan
	fmt.Printf("num3: %v\n", num3) // num3: 211
}

注意事项

  1. channel中只能存放指定的数据类型
  2. channel的数据放满后,就不能再放入了
  3. 若从channel取出数据后,可以继续放入
  4. 在没有使用协程的情况下,若channel数据取完了,再取的话,就会报 deadlock

案例

func main() {
	// 1.
	var intChan chan int = make(chan int, 6)
	intChan <- 10
	intChan <- 20
	intChan <- 30
	num1 := <-intChan
	num2 := <-intChan
	num3 := <-intChan
	// num1: 10        num2: 20        num3: 30
	fmt.Printf("num1: %v\tnum2: %v\tnum3: %v\n", num1, num2, num3)

	// 2.
	var mapChan chan map[string]string = make(chan map[string]string, 5)
	var mapTemp1 map[string]string = make(map[string]string)
	mapTemp1["tom"] = "12"
	mapTemp1["jack"] = "23"
	var mapTemp2 map[string]string = make(map[string]string)
	mapTemp2["jacy"] = "12"
	mapTemp2["mary"] = "23"
	mapChan <- mapTemp1
	mapChan <- mapTemp2
	var mapTemp3 map[string]string = make(map[string]string)
	mapTemp3 = <-mapChan
	fmt.Printf("mapTemp3: %v\n", mapTemp3) // mapTemp3: map[jack:23 tom:12]

	// 3.
	var dog1 Dog = Dog{"tom", 22}
	var dog2 Dog = Dog{"jack", 33}
	var dogChan chan Dog = make(chan Dog, 5)
	dogChan <- dog1
	dogChan <- dog2
	var dog3 Dog = <-dogChan
	fmt.Printf("dog3: %v\n", dog3) // dog3: {tom 22}

	// 4.
	var dog4 Dog = Dog{"tom", 22}
	var dog5 Dog = Dog{"jack", 33}
	var dogChan2 chan *Dog = make(chan *Dog, 5)
	dogChan2 <- &dog4
	dogChan2 <- &dog5
	var dog6 = <-dogChan2
	fmt.Printf("dog6: %v\n", dog6) // dog6: &{tom 22}

	// 5.
	var allChan chan interface{} = make(chan interface{}, 5)
	allChan <- "hello"
	allChan <- 99
	allChan <- dog1
	allChan <- &dog2
	allChan <- mapTemp1
	str := <-allChan
	fmt.Printf("str: %v\n", str) // str: hello
	// 取出map
	<-allChan
	dog7 := <-allChan
	<-allChan
	mapTemp4 := <-allChan
	fmt.Printf("mapTemp4: %v\n", mapTemp4) // mapTemp4: map[jack:23 tom:12]
	fmt.Printf("dog7: %v\n", dog7)
	// 7.
	// dog7.Name  // 错误,因为类型是interface{}
	// 借助类型断言,进行转换
	d := dog7.(Dog)
	fmt.Printf("d.Name: %v\n", d.Name) // d.Name: tom

}

type Dog struct {
	Name string
	Age  int
}

channel遍历和关闭

channel关闭
  1. 使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
channel遍历
  1. channel支持for-range方式的遍历

  2. 在遍历时,若channel没有关闭,则会出现deadlock

  3. 在遍历时,若channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历

案例1
func main() {
	numChan := make(chan int, 2000)
	resChan := make(chan int, 2000)
	exitChan := make(chan int, 8)

	go writeNum(numChan)
	for i := 0; i < 8; i++ {
		go storeResult(resChan, numChan, exitChan)
	}

	for len(exitChan) != 8 {
	}
	close(resChan)

	i := 0
	for v := range resChan {
		i++
		fmt.Printf("res[%v]: %v\n", i, v)
	}

}

func writeNum(numChan chan int) {
	for i := 1; i <= 2000; i++ {
		numChan <- i
	}
	close(numChan)
}

func storeResult(resChan chan int, numChan chan int, exitChan chan int) {
	// 读取并计算保存
	for v := range numChan {
		res := 0
		for i := 1; i < v; i++ {
			res += i
		}
		resChan <- res
	}
	exitChan <- 1
}

案例2
import (
	"fmt"
	"time"
)

func main() {

	numChan := make(chan int, 1000)
	resChan := make(chan int, 2000)
	exitChan := make(chan bool, 4)

	startTime := time.Now().Unix()

	go writeData(numChan)

	for i := 0; i < 4; i++ {
		go primeStore(numChan, resChan, exitChan)
	}

	var endTime int64 = 0

	go func() {
		for len(exitChan) != 4 {

		}
		endTime = time.Now().Unix()
		close(resChan)
		close(exitChan)
	}()

	for v := range resChan {
		fmt.Printf("v: %v\t", v)
	}

	fmt.Println("main exit")
	fmt.Printf("startTime: %v\n", startTime)
	fmt.Printf("endTime: %v\n", endTime)
	fmt.Println("time: ", (endTime - startTime))
}

// 写入
func writeData(numChan chan int) {
	for i := 1; i <= 80000; i++ {
		numChan <- i
	}
	close(numChan)
}

// 判断是否是素数,并保存到resChan中
func primeStore(numChan chan int, resChan chan int, exitChan chan bool) {
	for v := range numChan {
		flag := true
		// 判断是否是素数
		for i := 2; i <= v/2; i++ {
			if v%i == 0 {
				flag = false
				break
			}
		}
		if flag {
			resChan <- v
		}
	}
	exitChan <- true

	fmt.Println("prime exit")
}
注意事项和细节
  1. channel可以声明为只读,或者只写性质
func main() {
	// 1. 在默认情况下,channel是双向的
	var chan1 chan int = make(chan int, 2)
	fmt.Printf("chan1: %v\n", chan1)
	// 2. 只写
	var chan2 chan<- int = make(chan int, 3)
	// num := <-chan2 //cannot receive from send-only channel chan2
	fmt.Printf("chan2: %v\n", chan2)
	// 3. 只读
	var chan3 <-chan int = make(chan int, 4)
	// chan3 <- 3 // cannot send to receive-only channel chan3
	fmt.Printf("chan3: %v\n", chan3)
}

  1. 使用Select可以解决从管道取数据的阻塞问题
func main() {
	intChan := make(chan int, 10)
	stringChan := make(chan string, 5)
	for i := 0; i < 10; i++ {
		intChan <- i + 1
	}
	for i := 0; i < 5; i++ {
		stringChan <- "string" + strconv.Itoa(i+1)
	}
	// 当不关闭channel,用传统的方法在遍历channel时,会导致deadlock
	// 可以使用select 方式解决问题
	for {
		select {
		case v := <-intChan:
			fmt.Printf("intChan v: %v\n", v)
			time.Sleep(time.Second)
		case v := <-stringChan:
			fmt.Printf("stringChan v: %v\n", v)
		default:
			fmt.Println("exit")
			return
		}
	}

}

image-20220511200707968

  1. goroutine中使用recover,解决goroutine中出现的panic,导致程序崩溃问题
import (
	"fmt"
	"time"
)

func sayHello() {
	for i := 0; i < 10; i++ {
		fmt.Printf("Hello-%v\n", i+1)
		time.Sleep(time.Second)
	}
}

func test() {

	defer func() {
		err := recover()
		if err != nil {
			fmt.Printf("err: %v\n", err)
		}
	}()

	// 存在错误,会影响整个goroutine和main的执行
	var myMap map[int]string
	myMap[1] = "tom"
}

func main() {

	go sayHello()
	go test()

	for i := 0; i < 10; i++ {
		fmt.Printf("go-%v\n", i+1)
		time.Sleep(time.Second)
	}

}

image-20220511200630536

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SoaringW

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值