Go_12_竞态

  • 竞态
    • 互斥锁
    • 读写锁
    • 初始锁
    • 上下文
      • emptyCtx
      • cancelCtx
      • timerCtx
      • valueCtx
      • 常用函数
  • 通道
    • 无缓冲通道
    • 缓冲通道
    • 单向通道
    • 多路复用
    • 实现原理

竞态

竞态(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 intfunc (*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 {Contextmu       sync.Mutex                // 互斥锁(保证关闭期间的线程安全)done     atomic.Value            // 获取关闭context的通知children map[canceler]struct{}     // 记录子goroutineerr      error                   // 关闭原因
}

使用WithCancel()函数须知:

1)cancelCtx实例的父节点必须也可被关闭类型;

2)若父节点不支持关闭,则继续向上查询直至找到支持可关闭的类型;

3)若均不支持被关闭则启动个goroutine做伪父节点,同时监控原父节点;

//启动的goroutine会在父节点结束时,通知cancelCtx实例执行cancel()方法

timerCtx

timerCtx:基于cancelCtx上添加达到存活/过期时间自动关闭的功能

1)timerCtx的结构体原型如下:

type timerCtx struct {cancelCtxtimer    *time.Timer // 触发自动关闭的定时器deadline time.Time   // 记录关闭的最终时间
}

使用WithTimeout()函数须知:

1)需指定过期时间;

2)本质:将过期时间转换为存活时间,并调用WithDeadline()函数;

valueCtx

valueCtx:在Context基础上添加多级goroutine共享键值对数据的功能

1)子节点查找对应键的数据时,会依次遍历所有父节点;

2)若所有父节点无对应键,则返回interface{};

3)valueCtx的结构体原型如下:

type valueCtx struct {Contextkey, val interface{} // 任意类型键值对
}

使用WithValue()函数须知:

1)指定的键值对包含创建的valueCtx实例(子节点);

2)创建的valueCtx实例无法关闭(其Done()方法无法返回);

//可给其指定支持关闭的父节点,在需关闭时关闭父节点和子节点

如:使用WithValue创建父子context,并传输数据和关闭

1)编写程序;

package mainimport ("context""fmt"
)type keytypeA stringtype 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
//常用创建父级context1)返回个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

如:向通道写数据的流程

如:从通道读数据的流程

Go(Golang)_12_竞态相关推荐

  1. golang race 竞态检测

    -race 属性用来做竞态监测,判断是否存在并发读写单个变量的情况.在执行 go test.go build.go run 的时候都可以执行这个参数. 这个检测是发生在运行时的,如果你的代码没有被执行 ...

  2. golang data race 竞态条件

    golang race condition 竞态条件 data race race condition golang race detector golang的协程机制使得编写并发代码变得非常容易,但 ...

  3. eclipse运行go test_在 Go 中发现竞态条件 (Race Conditions)

    当我意识到我一直在处理和解决的问题有一个专有名词描述的时候,我总会觉得这事十分有趣.这次出现这种情况的是竞争条件(Race Conditions).当你处理多个 routine 共享某类资源的时候,不 ...

  4. 竞态条件的赋值_《Java并发编程实战》读书笔记一:基础知识

    一.线程安全性 一个对象是否是需要是线性安全的,取决于它是否需要被多个线程访问 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步,这个 ...

  5. 线程学习5——竞态条件

    竞态条件 概述:如果两个或两个以上的线程同时访问相同的对象,或者访问不同步的共享状态.就会出现竞态条件. 举例:如果多个线程同时访问类StateThread中的方法,最后结果会如何呢? 定义一个类St ...

  6. Linux系统时间和时序,什么是时序竞态 Linux系统时序竞态问题分析

    什么是时序竞态?将同一个程序执行两次,正常情况下,前后两次执行得到的结果应该是一样的.但由于系统资源竞争的原因,前后两次执行的结果有可能得到不一样的结果,这个现象就是时序竞态.时序竞态前导例 在讲时序 ...

  7. 【Linux开发】linux设备驱动归纳总结(四):5.多处理器下的竞态和并发

    linux设备驱动归纳总结(四):5.多处理器下的竞态和并发 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...

  8. Linux内核的并发与竞态、信号量、互斥锁、自旋锁

    /************************************************************************************ *本文为个人学习记录,如有错 ...

  9. 关于JavaScript并发、竞态场景下的一些思考和解决方案

    前言 时间是程序里最复杂的因素 编写 Web 应用的时候,一般来说,我们大多时候处理的都是同步的.线性的业务逻辑.但是正如开篇所说的"时间是程序里最复杂的因素",应用一旦复杂,往往 ...

最新文章

  1. vim 7.4同时支持python 2.x和3.x问题调研
  2. EFQRCode:自动生成花式二维码
  3. hdu4279 找规律+小想法
  4. android获取自定义属性,android 自定义控件中获取属性的三种方式(转)
  5. Python如何打包EXE可执行文件
  6. 高效率学习Java编程提升自我
  7. SAP成都研究院数字创新空间小伙伴们在2020 SAP上海DKOM
  8. genymotion 极速模拟器
  9. 360发布穿戴设备“儿童卫士”手环
  10. phoenix hbase java_java jdbc访问hbase phoenix
  11. quantaxis使用docker安装,解决了一个很奇特的问题
  12. Java进阶之网络编程
  13. webClient请求JAVA超时解决方案
  14. 计算机网络系统组播功能_全国计算机等级考试四级计算机网络考试大纲(最新版2018年版)...
  15. 设计模式的征途—7.适配器(Adapter)模式
  16. Atitit 前后端交互模式 目录 1.1. Ajax 1 1.2. Fetch api 1 1.3. 服务端脚本模式(简单快速) 1 1.4. 浏览器注入对象、函数 1 1.5. 浏览器插件模式
  17. ENVI 工具箱汉翻译汉化
  18. 华为路由器接口如何区分_华为新一代路由评测,自带NFC,一碰就能联网
  19. macos可以升级到指定版本吗_[macOS]如何升级更新 Mac 系统
  20. 算法学习之狄克斯特拉算法

热门文章

  1. CISCO和华为交换机修改密码
  2. 技术专栏|多旋翼飞行器振动机理分析和减振设计
  3. 毕业设计-后台管理系统
  4. SIMD和SPMD的区别
  5. API是用来干什么的
  6. 【计算机网络】 课程大作业:利用Wireshark抓包并进行分析
  7. 国产M0核风机量产程序开发方案… FOC电机控制开发方案…3电阻采样
  8. 金额的每三位一个逗号的正则解法
  9. 二叉搜索树BST的学习
  10. 深度学习模型DNN部署到安卓设备上全流程示例——{pytorch->onnx>ncnn->Android studio}