Go语言头秃之路(六)

本文深入探讨Go语言的协程特性,包括协程的轻量级线程属性、与线程的区别、Go的MPG模式、互斥锁与读写锁、管道channel的使用、并发安全与死锁预防,以及context在控制协程同步关闭中的应用。通过实例解析,帮助理解Go语言的并发优势和资源管理。

更新系列

Go语言头秃之路(零)
Go语言头秃之路(一)
Go语言头秃之路(二)
Go语言头秃之路(三)
Go语言头秃之路(四)
Go语言头秃之路(五)


  • 协程(goroutine)

    • 协程特点:(面试)
    1. 有独立的栈空间;
    2. 共享程序堆空间;
    3. 调度由用户控制;
    4. 协程是轻量级的线程。
    • 线程和协程
    1. 主线程是一个物理线程,是内核态,直接作用在CPU上,比较耗费CPU资源;
    2. 协程从主线程开启,是轻量级线程,是逻辑态,对资源消耗相对小;
    3. 如果主线程退出了,则线程即使还没有执行完毕,也会退出;(主死从随)
    4. golang的协程机制是重要的特点,可以轻松的开启上万个协程。(并发优势)
    5. 与go相比,python协程占用cpu/内存资源较少(如下程序运行时观察资源使用情况)
      package main
      
      import (
      	"fmt"
      	"sync"
      )
      
      var wg sync.WaitGroup
      
      func main() {
          // Add数量必须和Done数量相同
      	wg.Add(5)
      	for i := 0; i < 5; i++ {
      		go func(n int) {
      			defer wg.Done()
      			fmt.Println(n)
      		}(i)
      	}
      	wg.Wait()
      }
      
      
      import asyncio
      
      
      async def func1(n):
          while True:
              print(n)
              await asyncio.sleep(1)
      
      
      async def main():
          for i in range(100000):
              # 运行协程,阻塞协程直到返回结果
              # await func1(i)
              asyncio.create_task(func1(i))
          await asyncio.sleep(10)
      
      
      asyncio.run(main())
      
    • MPG模式
    1. M:操作系统主线程(物理线程)
    2. P:协程执行需要的上下文(需要的资源)
    3. G:协程
      运行状态描述:
      在这里插入图片描述
    • 设置使用逻辑CPU个数 runtime包
      runtime.NumCPU():查看程序使用CPU个数,在go1.8之后默认使用多个cpu;
      runtime.GOMAXPROCS(2):设置程序使用cpu个数,在go1.8之前版本设置后可以提高程序执行效率。

    • 互斥锁:解决资源竞争(low版)
      sync.Mutex
      能不用锁尽量不用:性能损耗

      // 使用goroutinue计算10的阶乘
      package main
      
      import (
      	"fmt"
      	"sync"
      	"time"
      )
      
      var (
      	jiecheng = make(map[int]uint, 1)
      	// 全局互斥锁
      	lock sync.Mutex
      )
      
      func jc(n int) {
      	var res uint = 1
      	for i := 1; i <= n; i++ {
      		res *= uint(i)
      	}
      	lock.Lock()
      	jiecheng[n] = res
      	lock.Unlock()
      }
      
      func main()  {
      	for i := 1; i <= 10; i++ {
      		go jc(i)
      	}
      
      	// 解决:fatal error: concurrent map read and map write
      	time.Sleep(time.Second * 10)
      
      	lock.Lock()
      	for j := 1; j <= 10; j++ {
      		fmt.Printf("%d 的阶乘为 %d\n", j, jiecheng[j])
      	}
      	lock.Unlock()
      }
      
    • 读写锁
      sync.RWMutex

      package main
      
      import (
      	"fmt"
      	"sync"
      	"time"
      )
      
      var wg sync.WaitGroup
      
      // 读写锁
      var rwLock sync.RWMutex
      
      func read() {
      	defer wg.Done()
      	rwLock.RLock()
      	fmt.Println("开始读取...")
      	time.Sleep(time.Second)
      	fmt.Println("读取完成。")
      	rwLock.RUnlock()
      }
      
      func write() {
      	defer wg.Done()
      	rwLock.Lock()
      	fmt.Println("开始写入...")
      	time.Sleep(time.Second)
      	fmt.Println("写入完成。")
      	rwLock.Unlock()
      }
      
      func main() {
      	wg.Add(6)
      	for i := 0; i < 5; i++ {
      		go read()
      	}
      	go write()
      	wg.Wait()
      }
      
  • 管道(channel)
    channel本质是一个数据结构 - 队列;
    数据按照先进先出原则(FIFO);
    管道是线程安全的,多goroutinue访问时,不需要加锁;
    channel是有类型的,一个string的channel只能存放string类型数据。

    声明语法:var 变量名 chan 数据类型
    eg:var intChan chan int
    channel是引用类型,必须初始化(make)才能写入数据,管道make的时候定义的数据容量不能更改
    从管道取一个数据后,管道长度会 -1 ,而容量不变。
    无论容量存满再放入数据还是从空管道取数据都会报死锁(deadlock!)
    使用任意类型的管道,即接口interface{}需要使用其中的属性或方法时,需要先进行类型断言
    遍历管道最好使用 for v:= range channel方式,注意这里只接收一个值v
    管道遍历前需要关闭管道close(channel),如果管道没有关闭,最后会报错deadlock
    如果编译器发现一个管道只有写没有读,则该管道会阻塞死锁(deadlock!)
    如果写管道和读管道的频率不一致,小炒面不报错。

    type Cat struct {
    	Name string
    	Age int
    }
    var intChan chan interface{}
    intChan = make(chan interface{}, 3)
    cat1 := Cat{"miaomiao", 1}
    // 往管道写入数据
    intChan <- 6
    intChan <- cat1 
    fmt.Printf("intChan:%v \t %T\n", intChan, intChan)
    fmt.Printf("len: %v, cap: %v\n", len(intChan), cap(intChan))
    // 从管道读取数据,丢弃则不写变量,如<- intChan
    n1 := <- intChan
    fmt.Printf("len: %v, cap: %v\n", len(intChan), cap(intChan))
    getCat := <- intChan
    // 这里直接取getCat.Name编译不通过
    cat2 := getCat.(Cat)
    fmt.Println(cat2.Name)
    // 遍历前关闭管道,管道关闭后不能再写入数据,但是可以读取数据
    close(intChan)
    // 遍历管道
    for v := range intChan {
    	fmt.Println(v)
    }
    
    • 注意事项:
    1. 默认情况下,管道为双向(可读可写),对于一些特殊情况(一般在函数参数中,防止误操作)可以将管道声明为只读(receive-only 箭头在前面)或只写(send-only 箭头在后面)。
      双向管道可以以参数形式传递给单项管道。
      eg:只读 intChan <- chan int
      只写 intChan chan <- int

    2. 当不确定何时关闭管道时,使用select语句

      intChan := make(chan int, 3)
      for i := 0; i < 3; i++ {
      	intChan <- i
      }
      // defer close(intChan)
      
      stringChan := make(chan string, 6)
      for j := 0; j < 6; j++ {
      	stringChan <- "hello" + fmt.Sprint(j)
      }
      // defer close(stringChan)
      
      for {
      	select {
      		// 使用select语句,即使管道没有关闭也不会报 deadlock 
      		case v := <- intChan:
      			fmt.Println("从intChan取到:", v)
      		case v2 := <- stringChan:
      			fmt.Println("从stringChan取到:", v2)
      		default:
      			fmt.Println("取不到数啦。。")
      			return
      	}
      }
      
    3. context:控制主协程和子协程同步关闭
      ctx, cancel := context.WithCancel(parent Context):需要手动执行cancel()
      传入的是父context,当父context退出时,子context也会跟着退出.
      ctx, cancel := context.WithTimeout(parent Context, timeout time.Duratio):指定超时时间后自动关闭,不需要手动执行cancel(),当然也可以手动执行。
      ctx, cancel := context.WithDeadline(parent Context, d time.Time):到了哪个时间自动退出。
      在web开发中比较常用。

      package main
      
      import (
      	"context"
      	"fmt"
      	"sync"
      	"time"
      )
      
      var wg sync.WaitGroup
      
      func cpuInfo(ctx context.Context) {
      	defer wg.Done()
      	ctxx, _ := context.WithCancel(ctx)
      	go memoryInfo(ctxx)
      	for {
      		select {
      		case <-ctx.Done():
      			fmt.Println("退出CPU监控。")
      			return
      		default:
      			fmt.Println("实时CPU监控...")
      			time.Sleep(time.Second)
      		}
      	}
      }
      
      func memoryInfo(ctxx context.Context) {
      	defer wg.Done()
      	for {
      		select {
      		case <-ctxx.Done():
      			fmt.Println("退出内存监控。")
      			return
      		default:
      			fmt.Println("实时内存监控...")
      			time.Sleep(time.Second)
      		}
      	}
      }
      
      func main() {
      	wg.Add(2)
      	// context.WithCancel()中传入的是父context,当父context退出时,子context也会跟着退出
      	ctx, cancel := context.WithCancel(context.Background())
      	go cpuInfo(ctx)
      	time.Sleep(time.Second * 3)
      	cancel()
      	wg.Wait()
      }
      
    4. 防止因为某个协程报错而整个程序异常退出:
      在协程中使用defer+recover来捕获panic。

      // 在方法开头加入代码
      defer func() {
      	if err := recover(); err != nil {
      		fmt.Println("发送邮件:执行报错了", err)
      	}
      }()
      

      panic会引起主线程挂掉,同时会导致其他的协程都挂掉;
      在父协程中无法捕获子协程中出现的异常,需要分别进行捕获。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值