进程、线程、协程
- 进程:操作系统分配资源的最小单元。
- 线程:CPU调度的最小单位(程序执行流的最小单元),是进程的一个执行单元
- 协程:用户级线程,相比较线程而言,更轻量,调度由程序自身控制。
多进程和多线程存在的问题
线程切换消耗资源多。(php、python启动一个线程的调度工作给操作系统做,而java则给jvm去调度(jvm实际也是给操作系统做)。)
并发和并行
并发是任务的交替执行(如:时间片轮转进程调度算法),而并行是任务的同时执行。
Goroutine
go的协程,也叫轻量级线程,绿程。
main函数也是routine,只不过他是主routine
协程优点
1、内存占用小
2、切换快。
使用go关键字创建子routine
go func(){}
package main
import (
"fmt"
"time"
)
// 主协程
func main() {
go func() {
fmt.Println("子协程")
}()
fmt.Println("主协程")
//主死随从,不等待一会,子协议还没打印程序就结束了
time.Sleep(2 * time.Second) // 演出
}
/*
* 主协程
* 子协程
*/
package main
import (
"fmt"
"time"
)
// 主协程
func main() {
// 匿名函数启动goroutine
for i := 0; i < 100; i++ {
go func(i int) {
fmt.Println(i)
}(i) // 闭包问题
}
time.Sleep(2 * time.Second)
}
/*
* 结果:无序的输出1到100
* 5
* 15
* 15
* ...
* 87
* 91
*/
gmp
waitgroup
作用:等待一组协程执行完毕。在设定等待Goroutine数量执行完成前阻塞,等设定Goroutine数量执行完毕才会解除阻塞。
用法:在父协程调用Add方法来设定等待的协程数量。每个被等待协程应在结束时调用Done方法。同时,主协程里可以调用Wait方法阻塞至所有协程结束。
缺点:在实际应用中,一般是等待一个协程组,若流程正常,则等待所有协层结束。反之,如果其中一个协程异常,则会结束整个协程组停止运行释放资源。waitgroup是无法实现的这个功能的。(可以用channel实现)
初始化
var wg sync.WaitGroup //var声明waitgroup不需要在额外进行显示初始化操作,go会初始化为零值状态
wait := sync.WaitGroup{} // 短变量声明
数据结构
type WaitGroup struct {
noCopy noCopy
state atomic.Uint64 // / 高 32 bit 是计数值(counter), 低 32 bit 是 waiter 的计数。
sema uint32
}
noCopy
一个特殊的结构体,表示该值不允许值复制,用go vet可以检查出该错误。
state
sema
信号量
三个内置方法
- Add : 用于向 WaitGroup 中添加指定数量的等待的 goroutine,即修改state中的counter值
- Done: 结束就是 Add(-1)
- Wait:等待一组goroutine执行完毕,Wait会检查 counter 的值,如果不为 0,则当前的 goroutine 会被阻塞。当 counter 的值为 0 时,阻塞解除
package main
import (
"fmt"
"sync"
)
// 主协程
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i) // 闭包问题
}
//等到监控的子协层结束
wg.Wait()
fmt.Println("执行完成")
}
/*
* 结果:无序的输出1到100
* 5
* 15
* 15
* ...
* 87
* 91
*/
锁
并发会存在一个竞争问题,锁是用来解决这个问题的。
锁的本质就是将并发的代码进行串行化了,所以使用锁肯定会影响性能,所以设计锁的时候尽量保持并行。
tips: 锁复制了就失去锁的作用的。所以在使用时不能进行复制,不过复制了也不会报错。
互斥锁
互斥锁(Mutex)是一种用于多线程编程的同步机制。
例子: 非原子化操作。
package main
import (
"fmt"
"strconv"
"sync"
)
var wg sync.WaitGroup
var total int
func add() {
defer wg.Done()
total += 1
}
func sub() {
defer wg.Done()
total -= 1
}
// 主协程
func main() {
for i := 0; i < 10000; i++ {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Println("第" + strconv.Itoa(i) + "次,值为" + strconv.Itoa(total))
}
}
/*
* 结果:并不是都是0
* 第0次,值为0
* ...
* 第7634次,值为1
* ...
* 第9978次,值为2
* ...
*/
原因:
结果并不一样等于0,出现这个情况的原因是total += 1并不是原子化操作
total += 1分为三步,
- 读取total ,
- 计算total + 1,
- 计算结果写入到a。
total -= 1也是同理。
而这六步会存在穿插执行,所以导致结果不一样。
解决方法就是将total += 1变为原子化。
两个方法
1、使用互斥锁Mutex
2、使用atomic包
package main
import (
"fmt"
"strconv"
"sync"
)
var wg sync.WaitGroup
var total int
var lock sync.Mutex
func add() {
lock.Lock()
total += 1
lock.Unlock()
wg.Done()
}
func sub() {
lock.Lock()
total -= 1
lock.Unlock()
wg.Done()
}
// 主协程
func main() {
for i := 0; i < 100000; i++ {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Println("第" + strconv.Itoa(i) + "次,值为" + strconv.Itoa(total))
}
}
/*
* 结果:都是0
* 第0次,值为0
* ...
* 第99999次,值为0
* ...
*/
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var wg sync.WaitGroup
var total int32
var lock sync.Mutex
func add() {
atomic.AddInt32(&total, 1)
wg.Done()
}
func sub() {
atomic.AddInt32(&total, -1)
wg.Done()
}
// 主协程
func main() {
for i := 0; i < 100000; i++ {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Printf("第%d次,值为%d\r\n", i, total)
}
}
/*
* 结果:都是0
* 第0次,值为0
* ...
* 第99999次,值为0
* ...
*/
读写锁
读锁:允许读锁的goroutine进行访问,但不允许写锁定的gorouting进行操作,会阻塞到所有的读锁定的goroutine执行完毕才能进行操作
写锁: 当一个goruntine获取写锁定时候,其他goroutine都会被阻塞,直到写锁被释放。
为什么需要读写锁:
在web场景中,读数据远多于写数据。在多个gorouting的时候,存在三种情况。
- 读与读,需要并发
- 读与写、写与写, 需要互斥
- 写的过程不可读。(例如修改商品价格的情况。)
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var total int
var rwlock sync.RWMutex
func add() {
rwlock.Lock() // 加写锁,防止别的写、读执行。
total += 1
fmt.Printf("写入%d\r\n", total)
time.Sleep(3 * time.Second)
rwlock.Unlock()
wg.Done()
}
func sub() {
rwlock.RLock() /// 加读锁,不会阻止其他协程的读
time.Sleep(1000 * time.Millisecond)
fmt.Printf("读取%d\r\n", total)
rwlock.RUnlock()
wg.Done()
}
// 主协程
func main() {
wg.Add(2)
go add()
for i := 0; i < 100000; i++ {
wg.Add(1)
go sub()
}
go add()
wg.Wait()
fmt.Println("结束了")
}
// 结果遇到写的过程中阻塞3秒。在打印出读的操作
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go add()
}
for i := 0; i < 10; i++ {
wg.Add(1)
go sub()
}
wg.Wait()
fmt.Println("结束了")
}
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
var total int
var rwlock sync.RWMutex
func add() {
rwlock.Lock() // 加写锁,防止别的写、读执行。
total += 1
fmt.Printf("写入%d\r\n", total)
time.Sleep(3 * time.Second)
rwlock.Unlock()
wg.Done()
}
func sub() {
rwlock.RLock() /// 加读锁,不会阻止其他协程的读
time.Sleep(1000 * time.Millisecond)
fmt.Printf("读取%d\r\n", total)
rwlock.RUnlock()
wg.Done()
}
// 主协程
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go add()
}
for i := 0; i < 10; i++ {
wg.Add(1)
go sub()
}
wg.Wait()
fmt.Println("结束了")
}
/*
* 结果:
* 读取0
* 写入1
* 读取1
* 读取1
* 读取1
* 读取1
* 读取1
* 读取1
* 读取1
* 读取1
* 读取1
* 写入2
* 写入3
* 写入4
* 写入5
* 写入6
* 写入7
* 写入8
* 写入9
* 写入10
* 结束了
*/
channel
什么是channel?
channel 是用来做 goroutine 通信使用的,是goroutine 之间的通信机制。
Go 的并发哲学是:“不要通过共享内存通信;而是通过通信共享内存。”
声明channel
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan []int // 声明一个传递int切片的通道
tips:声明channel后必须要进行初始化才能使用,不然会发生阻塞。
声明并初始化channel
创建语法:make(chan [type], int)
ch1 := make(chan string) // 创建无缓冲channel,并指明channel中的数据为string,双端等待
ch2 = make(chan [type], Int)// 创建有缓冲channel,Int为最大缓存容量。当发送消息达到int条数会阻塞,Int为0时,即无缓冲
发送
ch1 <- "xxxx"
接收
x, ok := <- ch1 //从ch1中获取值。 ok表示是否是未关闭channel获取的,false是通道已关闭获取的。
无缓冲channel和有缓冲channel
无缓冲channel
只有存在接收操作时,才会接受发送操作,不然每次发送数据时,程序都会被阻塞。适用于通讯,B要立刻知道A是否已经完成
package main
import (
"fmt"
"time"
)
func main() {
msg := make(chan string, 0) //无缓存channel
go func(msg chan string) { // happen-before的机制
data := <-msg // 取值。 将msg的值取出复制给data
fmt.Println(data)
}(msg)
msg <- "存放值"
time.Sleep(3 * time.Second)
}
有缓冲channe
就是将发送和接收操作解耦。 缓冲区满前不会阻塞程序,适用于消费者和生产者之间的通信。
package main
import "fmt"
func main() {
msg := make(chan string, 1) // 如果channel的空间设置为0的话,放值进去会导致阻塞,死锁。
msg <- "存放值" // 将值存进msg
data := <-msg // 取值。 将msg的值取出复制给data
fmt.Println(data)
}
range在channel的应用
for range会一直读取channel的数据,直到channel关闭。
close(channel)
go的一个内置函数,用于关闭channel,参数
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
msg := make(chan int, 2)
wg.Add(1)
go func(msg chan int) {
defer wg.Done()
for data := range msg {
fmt.Println(data)
}
}(msg)
msg <- 1
msg <- 2
close(msg) // 关闭通道, 关闭后只能读,不能写入
wg.Wait()
}
单向channel和双向channel
默认情况下,channel是双向channel.
var ch2 chan <- float64 //单向channel, ch2只能写入float64数据
var ch3 <- chan int //单项的channel, ch3只能读取
tip: 单项不能变双向
func main() {
c := make(chan int, 3)
var send chan<- int = c //send-only
var read <-chan int = c //recv-only
send <- 222
num := <-read
fmt.Println(num) // 222
}
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() {
c := make(chan int)
go producer(c)
go consumer(c)
time.Sleep(3 * time.Second)
}
/*
*num=0
*num=1
*num=4
*num=9
*num=16
*num=25
*num=36
*num=49
*num=64
*num=81
*/
多路复用
select关键字
与switch很相似,用于监控多个channel,解决有多个goroutine都在执行时,当某个goroutine执行完毕后需要立刻知道。
特点
- select会执行第一个已就绪的channel,如果都没有就绪,则有default流程走default,然后退出select,没有default则一直阻塞到有channel就绪或panic。
- 如果有多个case已就绪,则执行随机的case。(为了防止饥饿)
- 空的select语句将被阻塞,直至panic;
tips:
- case后面不一定是读channel,也可以是写channel,只要是对channel的操作就可以;
- 如果某个分支channel是关闭的,如果是接受语句视为就绪状态,如果是发送则会报错(send on closed channel)。
解决难点场景
package main
import (
"fmt"
"time"
)
var done = make(chan string)
func g1() {
done <- "g1准备就绪!"
}
func g2() {
time.Sleep(1 * time.Second)
done <- "g2准备就绪!"
}
func main() {
go g1()
go g2()
ready := <-done
fmt.Println(ready)
}
/*
* g1准备就绪!
*/
如果只有单个cannel的时候,可以很简单这样解决,但如果多个channel的话就非常麻烦,select就是解决这个场景的。
package main
import (
"fmt"
)
func g1(ch chan string) {
ch <- "g1准备就绪!"
}
func g2(ch chan string) {
ch <- "g2准备就绪!"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go g1(ch1)
go g2(ch2)
for i := 0; i < 2; i++ {
select {
case g1 := <-ch1:
fmt.Println(g1)
case g2 := <-ch2:
fmt.Println(g2)
}
}
}
// 随机出现以下结果
/*
* 结果1
* g1准备就绪!
* g2准备就绪!
*/
/**
* 结果2
* g1准备就绪!
* g2s准备就绪
*/
常用场景-超时
package main
import (
"fmt"
"time"
)
func g1(ch chan string) {
ch <- "g1准备就绪!"
}
func g2(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "g2准备就绪!"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go g1(ch1)
go g2(ch2)
timer := time.NewTimer(5 * time.Second)
for {
select {
case g1 := <-ch1:
fmt.Println(g1)
case g2 := <-ch2:
fmt.Println(g2)
case <-timer.C:
fmt.Println("超时")
return
}
}
}
/*
* g1准备处理!
* g2准备处理!
* 超时
*/
go中channel的应用场景:
1、消息传递
2、信息广播
3、事件订阅和广播
4、任务分发
5、结果汇总
6、并发控制
7、同步和异步
...
编程题
使用两个goroutine交替打印序列,一个goroutine打印数字,另外一个grountine打印字母,最终效果:
12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728
package main
import (
"fmt"
"time"
)
var number, letter = make(chan bool), make(chan bool)
func printNumber() {
i := 1
for {
<-number
fmt.Printf("%d%d", i, i+1)
i += 2
letter <- true
}
}
func printLetter() {
var str = "ABCDEFGHIJKLMNOPQRSTUVWSYZ"
i := 0
for {
<-letter
if i >= 26 {
return
}
fmt.Print(str[i : i+2])
i += 2
number <- true
}
}
func main() {
go printNumber()
go printLetter()
number <- true
time.Sleep(100 * time.Second)
}
//12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WS2526YZ2728
context
作用:
一个用于在程序之间传递上下文信息。用来解决链式通讯共享内存,达到自上而下的通知效果。
tips: channel+ select方案在嵌套的时候,比如一个协程衍生多个协程,然后要同时关闭就比较麻烦。
主要两个功能:
1、携带键值对。
2、管理取消信号,分为主动取消和超时取消
参考资料:www.zhihu.com/tardis/zm/a…
创建上下文
context.Background()
返回一个非nil的空Context,一般是main函数中创建的顶级上下文
context.TODO()
返回一个非nil的空Context,一般是用在不知道用哪个上下文的时候,用来占位。
Background和TODO的区别
两个返回都是空Context,仅仅是用于语义化区分。一个是作为顶级上下文,一个用于占位。
context结构体
type Context interface {
Deadline() (deadline time.Time, ok bool) //返回context的截止时间
Done() <-chan struct{} // 取消信号,一个只读channel
Err() error // 返回 context 取消原因
Value(key any) any // 获取 key 对应的 value
}
创建派生 Context(子Context)
用这些方法创建出来是一个树形结构。如果父级的context关闭了,子context也会随之关闭。
WithCancel
用于主动取消,返回一个子上下文,以及一个通知取消的信号函数
ctx, cancel := context.WithCancel(context.Background())
WithDeadline
用于在某个时间自动取消
deadline := time.Now().Add(5 * time.Second)
deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
WithTimeout
用于超时后自动取消
timeoutCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
原理
就是调用WithDeadline,用当前时间加上超时的时间作为第二个参数。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithValue
用于设置当前上下文携带的键值对
parentCtx := context.WithValue(context.Background(), "userID", 123)
tip:如果需要传递多个参数,传递结构体做为值。
官方的使用建议
- Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.
- Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
- Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
- The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
-
不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
-
不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
-
不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
-
同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。
简单使用例子:
6秒后主动结束监控
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func cpuInfo(ctx context.Context) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("退出cpu监控")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("cpu信息")
}
}
}
func main() {
wg.Add(1)
ctx, cancel := context.WithCancel(context.Background())
go cpuInfo(ctx)
time.Sleep(6 * time.Second)
cancel() // 结束
wg.Wait()
fmt.Println("监控完成")
}
/**
* cpu信息
* cpu信息
* cpu信息
* 退出cpu监控
* 监控完成
*/
6秒后超时自动结束并传递userID
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func cpuInfo(ctx context.Context) {
defer wg.Done()
fmt.Println(ctx.Value("userID"))
for {
select {
case <-ctx.Done():
fmt.Println("退出cpu监控")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("cpu信息")
}
}
}
func main() {
wg.Add(1)
ctx, _ := context.WithTimeout(context.Background(), 6*time.Second)
parentCtx := context.WithValue(ctx, "userID", 123)
go cpuInfo(parentCtx)
wg.Wait()
fmt.Println("监控完成")
}
/**
* 123
* cpu信息
* cpu信息
* cpu信息
* 退出cpu监控
* 监控完成
*/