GO的并发编程|青训营

101 阅读3分钟

因为接口的性能太差,想要做并发编程提高性能,go的并发编程的好处,这里就不再赘述了。

协程 goroutine

Go 为了提供更容易使用的并发方法,使用了 goroutine 和 channel。goroutine 来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上。最关键的是,程序员看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。

Go语言的GMP(Goroutine-Machine-Processor)调度原理

在 Go 中,线程是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上。

image.png

  • 全局队列(Global Queue):存放等待运行的 G。

  • P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。

  • P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。

  • M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

很可耻的开始想自己总结,但是前人备述矣,大家想看更详细的调度原理请看这个链接juejin.cn/post/699509…

理论太枯燥就直接放带代码啦

通过waitgroup等待协程的执行

````gO`

package main

import (
   "fmt"
   "sync"
)

//子goroutine如何通知到主的goroutine自己结束了, 主的goroutine如何知道子的goroutine已经结束了

func main(){
   var wg sync.WaitGroup

   //我要监控多少个goroutine执行结束
   wg.Add(100)
   for i := 0; i<100; i++ {
      go func(i int) {
         defer wg.Done()

         fmt.Println(i)
      }(i)
   }

   //等到
   wg.Wait()
   fmt.Println("all done")

   //waitgroup主要用于goroutine的执行等到, Add方法要和Done方法配套
}

goroutine中的锁

互斥锁


package main

import (
   "fmt"
   "sync"
   "sync/atomic"
)

/*
锁 - 资源竞争
*/

var total int32
var wg sync.WaitGroup

//var lock sync.Mutex

// 锁能复制吗, 复制后就失去了锁的效果
func add() {
   defer wg.Done()
   for i := 0; i < 1000000; i++ {
      atomic.AddInt32(&total, 1) //原子操作,不会被打断
      //lock.Lock()
      //total += 1 //竞争
      //lock.Unlock()
   }
}

func sub() {
   defer wg.Done()
   for i := 0; i < 1000000; i++ {
      atomic.AddInt32(&total, -1) //原子操作,不会被打断
      //lock.Lock()
      //total -= 1
      //lock.Unlock()
   }
}

func main() {
   wg.Add(2)
   go add()
   go sub()
   wg.Wait()
   fmt.Println(total)
}

读写锁

package main

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

//锁本质上是将并行的代码串行化了, 使用lock肯定会影响性能
//即使是设计锁,那么也应该尽量的保证并行
// 我们有两组协程, 其中一组负责写数据,另一个组负责读数据,web系统中绝大部分场景都是读多写少,
// 虽然有多个goroutine,但是仔细分析我们会发现, 读协程之间应该并发, 读和写之间应该串行, 读和读之间也不应该并行
// 读写锁


func main() {
   var rwlock sync.RWMutex
   var wg sync.WaitGroup

   wg.Add(6)
   //写的goroutine
   go func() {
      time.Sleep(time.Second*3)
      defer wg.Done()
      rwlock.Lock() //加写锁, 写锁会防止别的写锁获取,和读锁获取
      defer rwlock.Unlock()
      fmt.Println("get write lock")
      time.Sleep(time.Second*5)
   }()


   // 读的goroutine
   for i:=0; i<5; i++ {
      go func() {
         defer wg.Done()
         for {
            rwlock.RLock() //加读锁, 读锁不会阻止别人的读
            time.Sleep(500*time.Millisecond)
            fmt.Println("get read lock")
            rwlock.RUnlock()
         }
      }()
   }

   wg.Wait()
}

goroutine之间的通信channel

不要通过共享内存来通信, 而要通过通信来实现内存共享

package main

import (
   "fmt"
   "time"
)

func main() {
   var msg chan string

   //无缓冲channel适用于 通知, B要第一时间知道A是否已经完成
   //有缓冲channel适用于消费者和生产者之间的通信
   /*
   go中channel的应用场景:
      1. 消息传递、消息过滤
      2. 信号广播
      3. 事件订阅和广播
      4. 任务分发
      5. 结果汇总
      6. 并发控制
      7. 同步和异步
      ...
    */

   //又缓冲和无缓冲的channel
   msg = make(chan string, 0) //channel的初始化值 如果为0的话,你放值进去会阻塞
   //msg = make(chan string, 0) //无缓冲的channel
   //msg = make(chan string, 10) //无缓冲的channel
   go func(msg chan string) { //go有一种happen-before的机制, 可以保障
      data := <- msg
      fmt.Println(data) 
   }(msg)

   msg <- "hello"//放值到channel中
   // waitgroup 如果少了done调用,容易出现deadlock, 无缓冲的channel也容易出现
   time.Sleep(time.Second*10)
}
package main

import (
   "fmt"
   "time"
)

func main() {
   var msg chan int
   //又缓冲和无缓冲的channel
   msg = make(chan int, 2) //channel的初始化值 如果为0的话,你放值进去会阻塞
   go func(msg chan int) { //go有一种happen-before的机制, 可以保障
      for data := range msg {
         fmt.Println(data)
      }
      fmt.Println("all done")
   }(msg)

   msg <- 1//放值到channel中
   msg <- 2//放值到channel中

   close(msg) //其他的编程语言有很大的区别
   d := <-msg //已经关闭的channel可以继续取值,但是不能再放值了
   fmt.Println(d)
   //msg <- 3//放值到channel中, 已经关闭的channel不能再放值了
   // waitgroup 如果少了done调用,容易出现deadlock, 无缓冲的channel也容易出现
   time.Sleep(time.Second*10)
}

单向channel

package main

import (
   "fmt"
   "time"
)

func producer(out chan<- int) {
   for i:=0; i<10; i++ {
      out <- i*i;
   }
   close(out)
}

func consumer(in <-chan int) {
   for num := range in {
      fmt.Printf("num=%d\r\n", num)
   }
}

func main() {
   //默认情况下, channel是双向的
   // 但是,我们经常一个channel作为参数进行传递,希望对方是单向使用
   //
   //var ch1 chan int //双向channel
   //var ch2 chan<- float64 // 单向channel,只能写入float64的数据
   //var ch3 <-chan int //单向的, 只能读取

   //c := make(chan int, 3)
   //var send chan<- int = c //send-only
   //var read <-chan int = c // recv-only
   //
   //send <- 1
   //<-read

   c := make(chan int)
   go producer(c)

   go consumer(c)

   time.Sleep(10*time.Second)
}

经典面试题:

使⽤两个goroutine交替打印序列,⼀个goroutine打印数字, 另外⼀个goroutine打印字⺟, 最终效果如下: 12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728

package main

import (
   "fmt"
   "time"
)

var number, letter = make(chan bool), make(chan bool)

func printNum() {
   i := 1
   for {
      //我怎么去做到, 应该此处, 等待另一个goroutine来通知我
      <-number
      fmt.Printf("%d%d", i, i+1)
      i += 2
      letter <- true
   }
}

func printLetter() {
   i := 0
   str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
   for {
      //我怎么去做到, 应该此处, 等待另一个goroutine来通知我
      <-letter
      if i >= len(str) {
         return
      }
      fmt.Print(str[i : i+2])
      i += 2
      number <- true
   }
}

func main() {
   
   go printNum()
   go printLetter()
   number <- true

   time.Sleep(time.Second * 100)
}

goroutine select

package main

import (
 "fmt"
 "time"
)

//很多时候我并不会多个goroutine写同一个 channel

func g1(ch chan struct{}) {
 time.Sleep(2*time.Second)
 ch <- struct{}{}
}

func g2(ch chan struct{}) {
 time.Sleep(3*time.Second)
 ch <- struct{}{}
}

func main() {
 //select 类似于 switch case语句, 但是select的功能和我们操作linux里面提供的io的select、poll、epoll
 //select 主要作用于多个channel

 //现在有个需求, 我们现在有两个goroutine都在执行, 但是呢我在主的goroutine中, 当某一个执行完成以后,这个时候我会立马知道
 g1Channel := make(chan struct{}, 1)
 g2Channel := make(chan struct{}, 2)
 //g1Channel <- struct{}{}
 //g2Channel <- struct{}{}
 go g1(g1Channel)
 go g2(g2Channel)

 //我要监控多个channel, 任何一个channel返回都知道
 // 1. 某一个分支就绪了就执行该分支 2. 如果两个都就绪了,先执行哪个, 随机的, 目的是什么: 防止饥饿
 // 应用场景
 timer := time.NewTimer(5*time.Second)
 for {
    select {
    case <- g1Channel:
       fmt.Println("g1 done")
    case <- g2Channel:
       fmt.Println("g2 done")
    case <- timer.C:
       fmt.Println("timeout")
       return
    }
 }
}

通过contex解决goroutine的信息传递

package main

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

var wg sync.WaitGroup

//我们新的需求,我可以主动退出监控程序
//共享变量
func cpuIInfo(ctx context.Context)  {
  // 这里能拿到一个请求的id
  fmt.Printf("tracid: %s\r\n", ctx.Value("traceid"))
  //记录一些日志,这次请求是哪个traceid打印的

  defer wg.Done()
  for {
     select {
     case <- ctx.Done():
        fmt.Println("退出cpu监控")
        return
     default:
        time.Sleep(2*time.Second)
        fmt.Println("cpu的信息")
     }
  }
}

func main() {
  //渐进式的方式
  // 有一个goroutine监控cpu的信息
  wg.Add(1)

  //context包提供了三种函数, withCancel, WithTimeout, WithValue
  //如果你的goroutine, 函数中,如果希望被控制, 超时、传值,但是我不希望影响我原来的接口信息的时候,函数参数中第一个参数就尽量的要加上一个ctx
  //1. ctx1, cancel1 := context.WithCancel(context.Background())
  //ctx2, _ := context.WithCancel(ctx1)

  //2. timeout 主动超时
  ctx, _ := context.WithTimeout(context.Background(), 6*time.Second)

  //3. WithDeadline 在时间点cancel

  //4. withValue
  valueCtx := context.WithValue(ctx, "traceid", "gjw12j")
  go cpuIInfo(valueCtx)
  wg.Wait()
  fmt.Println("监控完成")
}