竞态
竞态(Race Condition):多个goroutine在没同步时对共享资源进行读写操作
1)本质:多个goroutine在交错顺序执行时,程序无法确定共享资源正确结果;
2)对共享资源读写操作必须是原子化的(同一时刻仅能一个goroutine操作);
并发安全:对象在被多个goroutine调用时,没其他机制下仍能正常工作
1)并发安全类型:其所有可访问方法和字段皆是并发安全的;
2)包界别导出的对象通常认为是并发安全的;
//程序中拥有多个goroutine时,其执行顺序是无法确定的
共享资源实现并发安全的3种方式:
1)不修改共享资源(不切实际);
2)避免多个goroutine访问同一个共享资源(通道);
3)允许多个goroutine访问同一共享资源,但限定时间内仅一个访问(锁);
互斥锁
互斥锁(sync.Mutex):可对共享资源进行读写操作的锁(单读单写锁)
1)临界区:在上锁和释放锁区间可对共享资源进行读写操作;
2)可搭配defer关键词确保每次上锁后都会释放锁;
互斥锁对象的定义格式:var 对象名 sync.Mutex
1)上锁格式:对象名.Lock()
2)释放锁格式:对象名.Unlock()
如:通过互斥锁实现两个goroutine修改同一变量
1)编写程序;
2)运行结果;
读写锁
读写锁(sync.RWMutex):仅可对共享资源进行写操作的锁(多读单写锁)
1)外部函数获取锁后,其调用再次尝试获取锁会导致死锁(产生宕机);
2)允许多个读操作并发执行,但写操作仅能一个goroutine执行;
//常用于竞争激烈场景(普通场景下慢于读写锁)
读写锁对象的定义格式:var 对象名 sync.RWMutex
1)上写锁格式:对象名.Lock()
2)写锁释放格式:对象名.Unlock()
3)上读锁格式:对象名.Rlock()
4)读锁释放格式:对象名.RUnlock()
//上读锁后,禁止任何写操作(写锁同理,禁止任何读操作)
自旋模式:goroutine持续检测是否可获得锁
1)自旋模式下其他goroutine释放的锁会被自旋goroutine立即获得;
2)自旋模式可充分利用CPU,避免goroutine的切换;
3)饥饿模式下不允许进入自旋模式;
goroutine成为自旋模式的条件:
1)CPU核数大于1;
2)已自旋次数小于4;
3)GMP中的P的数量大于1;
4)goroutine调度机制中的可运行队列必须为空;
饥饿模式:goroutine长时间未得到运行
1)本质:goroutine两次阻塞之间大于1ms就标记为饥饿模式再阻塞;
2)被标记为饥饿模式的goroutine会被首先获得被释放的锁;
初始锁
初始锁(sync.Once):确保被调用函数仅执行一次
1)被调用的函数功能通常为对其他资源进行初始化;
2)基于读写锁实现;
初始锁对象的定义格式:var 对象名 sync.Once
1)调用函数格式:对象名.Do(函数名)
2)Do()方法判断函数是否被执行,未执行则执行该函数(反之pass);
如:通过初始锁在循环中仅执行一次函数
1)编写函数;
2)运行结果;
//可用于实现单例模式
上下文
上下文(Context):控制多级并发goroutine
1)Context包由1个接口,4种实现和6个函数组成;
2)Context接口原型如下(所有类型context均基于Context接口):
type Context interface {
// 返回个deadline和是否已设置deadline的标识
Deadline() (deadline time.Time, ok bool)
// 被关闭时返回个关闭channel(未关闭时,返回nil)
Done() <-chan struct{}
// 描述context关闭的原因(未关闭时,返回nil)
Err() error
// 在树状分布的goroutine之间共享键值对(未有对应键,返回nil)
Value(key interface{}) interface{}
}
emptyCtx
emptyCtx:共用全局变量
1)emptyCtx并未真正实现Context接口(仅是个整型别名);
2)emptyCtx相关原型如下:
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
Background()和TODO()函数可创建empty实例,其原型如下
1)创建实例
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
2)Background()函数
func Background() Context {
return background
}
3)TODO()函数
func TODO() Context {
return todo
}
cancelCtx
cancelCtx:基于Context上添加可安全关闭本身和子goroutine的功能
1)内部实现的cancel()方法可安全关闭本身和子goroutine;
2)cancelCtx的结构体原型如下:
type cancelCtx struct {
Context
mu sync.Mutex // 互斥锁(保证关闭期间的线程安全)
done atomic.Value // 获取关闭context的通知
children map[canceler]struct{} // 记录子goroutine
err error // 关闭原因
}
使用WithCancel()函数须知:
1)cancelCtx实例的父节点必须也可被关闭类型;
2)若父节点不支持关闭,则继续向上查询直至找到支持可关闭的类型;
3)若均不支持被关闭则启动个goroutine做伪父节点,同时监控原父节点;
//启动的goroutine会在父节点结束时,通知cancelCtx实例执行cancel()方法
timerCtx
timerCtx:基于cancelCtx上添加达到存活/过期时间自动关闭的功能
1)timerCtx的结构体原型如下:
type timerCtx struct {
cancelCtx
timer *time.Timer // 触发自动关闭的定时器
deadline time.Time // 记录关闭的最终时间
}
使用WithTimeout()函数须知:
1)需指定过期时间;
2)本质:将过期时间转换为存活时间,并调用WithDeadline()函数;
valueCtx
valueCtx:在Context基础上添加多级goroutine共享键值对数据的功能
1)子节点查找对应键的数据时,会依次遍历所有父节点;
2)若所有父节点无对应键,则返回interface{};
3)valueCtx的结构体原型如下:
type valueCtx struct {
Context
key, val interface{} // 任意类型键值对
}
使用WithValue()函数须知:
1)指定的键值对包含创建的valueCtx实例(子节点);
2)创建的valueCtx实例无法关闭(其Done()方法无法返回);
//可给其指定支持关闭的父节点,在需关闭时关闭父节点和子节点
如:使用WithValue创建父子context,并传输数据和关闭
1)编写程序;
package main
import (
"context"
"fmt"
)
type keytypeA string
type keytypeC string //当键名相同时,防止查询本身
func main() {
var keyA keytypeA = "keyA"
ctx, cancel := context.WithCancel(context.Background())
//使用empty实例做为父节点
ctxA := context.WithValue(ctx, keyA, "ValA")
var keyC keytypeC = "keyA"
ctxC := context.WithValue(ctxA, keyC, "eggo")
fmt.Println(ctxC.Value(keyA))
fmt.Println(ctxC.Value(keyC))
cancel() //调用WithCancel()放回的关闭方法
}
2)运行结果;
常用函数
(1)创建普通上下文
1)返回个empty实例
func Background() Context
//常用创建父级context
1)返回个empty实例
func TODO() Context
//常用于不确定使用何种context时(静态工具可检测)
(2)创建各种实列的上下文
1) 将指定context包装成cancelCtx结构体的实例,并返回cancel方法
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
2) 将指定context包装成timerCtx结构体的实例,并返回cancel方法
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
3) 将指定context包装成timerCtx结构体的实例,并返回cancel方法
func WithTimeout(parent Context, timeout time.Dureation) (Context, cancelFunc)
//逻辑上借用WithDeadline()函数实现
4) 将指定context包装成valueCtx结构体的实例
func WithValue(parent Context, key, val interface{}) Context
通道
通道(Channel):Go语言中实现goroutine之间的通信机制
1)通道的零值是nil,且同种数据类型的通道是可比较的;
2)通道和goroutine之间的关系为:多对多;
3)通道分为:无缓冲通道、缓冲通道;
4)可通过通道实现管道;
通道的定义分为:无缓冲通道、缓冲通道
(1)无缓冲通道:通道名 := make(chan 数据类型)
(2)缓冲通道:通道名 := make(chan 数据类型,容量)
1)当通道仅用于同步时,数据类型可定义为“struct{}”或bool;
2)也可使用var关键词声明通道(值为nil);
通道的主要操作为:发送、接收
(1)向指定通道中发送数据:通道 <- 数据
1)数据的数据类型需与通道的数据类型保持一致;
(2)从指定通道中接收数据:变量1, 变量2 := <- 通道
1)变量的数据类型默认和从通道取出的数据类型一致;
2)若省略变量1和变量2,代表从通道中取出数据并丢弃;
3)变量2的数据类型为bool,为false时代表该通道已关闭且读完;
//变量2的名称一般被定义为“ok
”
使用通道需注意的3个事项:
(1)通过for range循环变量通过,格式:
for 接收数据变量 := range 通道 {
程序段
}
1)当通道关闭后且已被读完,循环会自动结束;
2)其读写数据的阻塞机制与普通通道相同;
(2)通过通道实现信号量,格式:
定义信号量通道:通道名 := make(chan struct{},信号量数)
获取信号量:通道名 <- struct{} {}
释放信号量:<- 通道名
1)数据类型为struct{},因为其所占空间大小为0;
2)goroutine必须获取信号量才可继续执行,执行完需释放;
3)当信号量被抢占完时,剩余goroutine需等待信号量被释放再抢占;
(3)手动关闭指定通道格式:close(通道)
1)向关闭的通道发送数据,会发生宕机;
2)从关闭的通道接收数据,默认取出通道对应数据类型的零值;
3)若关闭通道时该通道还存在数据,则默认先取出通道中的剩余数据
//关闭通道的操作不是必须的(GC会根据该通道是否可访问对其自动回收)
无缓冲通道
无缓冲通道(Unbuffered Channel):通道中数据即发即收(可理解容量为1)
1)无缓冲通道会导致限制发送或接收的goroutine处于阻塞等待状态;
2)无缓冲通道可实现发送和接收的goroutine处于同一时间交换数据;
goroutine泄露:运行较缓慢的goroutine使用无缓冲通道导致数据丢失
1)GC不会自动回收泄露的goroutine(需手动回收)
如:2个goroutine通过无缓冲通道传递数据
缓冲通道
缓冲通道(Buffered Channel):在通道的基础上添加一个元素队列
1)向缓冲通道发送的数据:元素队列的末尾处添加;
2)从缓冲通道中接收数据:从元素队列的头部取出;
3)缓冲通道填满时:发送的goroutine会阻塞到该缓冲通道可接收数据时;
4)缓冲通道为空时:接收的goroutine会阻塞到该缓冲通道有数据可接收时;
//len(通道名)返回当前通道含有的元素个数,cap(通道名)返回通道的容量
如:2个goroutine通过缓冲通道传递数据
单向通道
单向通道:通道被当成函数参数时,可限定其操作范围
1)单向通道分为:仅发送单向通道、仅接收单向通道
2)双向通道可隐式转换为任意单向通道(反之不可);
单向通道的定义格式(需在函数定义中):
1)仅发送单向通道:func 函数名(参数 chan <- 数据类型){}
2)仅接收单向通道:func 函数名(参数 <- chan 数据类型){}
//双向通道的定义格式为:func 函数名(参数 chan 数据类型) {}
如:在函数中定义单向通道
1)编写程序;
2)运行结果;
多路复用
多路复用(select):通过指定通道的操作决定执行分支程序段
1)当同时多个分支满足情况时,编译器会随机选择个分支执行;
2)若通道的值为nil,则该分支永远都不会被执行;
//可使用无任何分支的select语句实现永久阻塞
多路复用的定义格式:
select {
case 通道操作1:
程序段
case 通道操作N:
程序段
default:
程序段
}
1)若匹配不到特定操作时,执行默认的default分支的程序段;
2)若省略default语句,则匹配不到操作时select语句会永久阻塞;
3)搭配for循环使用时,跳出循环需使用return语句(break只能跳出select);
如:通过多路复用实现偶数输出
1)编写程序;
2)运行结果;
//也可用于监控多个不同的通道
实现原理
runtime/chan.go中通道的数据结构定义:
type hchan struct {
qcount uint // 环形队列中剩余的元素(数据)个数
dataqsiz uint // 环形队列长度(可存储元素的个数)
buf unsafe.Pointer // 指向环形队列的指针
elemsize uint16 // 每个元素的大小
closed uint32 // 是否关闭
elemtype *_type // 元素(数据)类型
sendx uint // 队列下标:指定元素写入时在队列中的位置
recvx uint // 队列下标:指定下个本读取元素在队列中的位置
recvq waitq // 等待读数据的协程队列
sendq waitq // 等待写数据的协程队列
lock mutex // 互斥锁
}
1)环形队列:内存中用于存储通道数据的缓冲区;
2)任何情况下recvq和sendq至少有一个为空(select语句除外);
3)创建通道的本质:初始化hchan结构体(内部调用make()函数实现);
关闭通道时会同时唤醒recvq和sendq队列中的G
1)recvq中的G会获取对应数据类型的零值;
2)sendq中的G会触发panic;
//关闭值为nil的通道也会触发panic
如:向通道写数据的流程
如:从通道读数据的流程