因为接口的性能太差,想要做并发编程提高性能,go的并发编程的好处,这里就不再赘述了。
协程 goroutine
Go 为了提供更容易使用的并发方法,使用了 goroutine 和 channel。goroutine 来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上。最关键的是,程序员看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。
Go语言的GMP(Goroutine-Machine-Processor)调度原理
在 Go 中,线程是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上。
-
全局队列(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("监控完成")
}