Go(Golang)_12_竞态

@

竞态

竞态(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


如:向通道写数据的流程
在这里插入图片描述

如:从通道读数据的流程
在这里插入图片描述

posted @ 2022-05-14 10:01  爱和可乐的w  阅读(88)  评论(0编辑  收藏  举报