go并发控制基础 - goroutine/chan/select/context/sync/timer

goroutine

goroutine是golang中的并发执行单元,是一种轻量级的线程(或称为协程),用于并发执行函数或方法。每个goroutine都有自己的调用栈,并独立地运行于其他goroutine之间,由Go运行时系统进行管理和调度。

创建一个新的goroutine非常简单,只需要在函数或方法前面加上关键字"go"即可。

通过使用goroutine,我们可以在程序中轻松地实现并发处理任务,提高程序的并发性和响应能力。

chan

Do not communicate by sharing memory; instead, share memory by communicating.

"不要以共享内存的方式来通信,相反,要通过通信来共享内存。"

基于CSP并发模型golang设计了channel这种数据结构来实现goroutine之间的数据传输和同步。

go 复制代码
rw := make(chan int)  // 可读可写channel
var r <-chan int // 只读channel,只能对该channel进行读操作
var w chan<- int // 只写channel,只能对该channel进行写操作

go func(ch chan int /* 引用传递 */) {
    unbuf := make(chan int)  // 创建无缓冲channel
    buf := make(chan int, 0) // 创建有缓冲channel,缓冲大小为 0,和无缓冲channel等价
    for i:= range buf {
        // 循环迭代读取缓冲chan中的数据
    }
    i := <- ch     // 从channel读数据
    ch <- i        // 写数据到channel中
    close(ch)      // 关闭channel
    i, ok := <-ch  // 从channel读数据
}(rw)

使用注意事项:

  1. chan需要采用make进行创建和初始化
  2. 对只读chan进行写操作、对只写chan进行读操作会导致编译错误
  3. chan是一个引用类型,作为函数入参或返回值传递时进行浅拷贝
  4. chan中数据的读写是进行值复制,因此有性能损耗

channel存在3种状态

  1. nil,未初始化的状态,只进行了声明,或者手动赋值为nil
  2. active,正常的channel,可读或者可写
  3. closed,已关闭,千万不要误认为关闭channel后,channel的值是nil

channel可进行3种操作

  1. 关闭

把这3种操作和3种channel状态可以组合出9种情况

操作 nil的channel 正常channel 已关闭channel
<- ch (读) 阻塞 成功或阻塞 读到零值
ch <- (写) 阻塞 成功或阻塞 panic
close(ch) (关闭) panic 成功 panic

对于nil通道的情况,也并非完全遵循上表,有1个特殊场景 :当nil的通道在select的某个case中时,这个case会阻塞,但不会造成死锁。

select-case-default

select语句用于在多个case通道操作中进行选择。它可以同时等待多个通道的操作,并执行第一个可用的操作,如果有多个case通道操作可用,则会随机选择一个执行

go 复制代码
func (ctx context.Context, ch chan int) {
    select {
    case <- ch:  // 通道可用
        // ...
    case <- time.After(time.Second):  // 操作超时处理
        // ...
    case ctx.Done():  // 上下文完成结束通知
        // ...
    default:  // 未命中任何case
        // ...
    }
}

使用注意事项:

  1. 没有任何 case 的 select 语句会被编译器转换为runtime.block()函数,永久阻塞
  2. 只有一个 channel 操作,实际会被编译器转换为相应channel 相应的收发操作,其实和实际调用 data := <- ch 并没有什么区别
  3. select 里面即可以对 channel 进行读取,还可以对 channel 进行写入,如果所有条件都不满足,并且有 default 子句,则执行 default 子句,否则,会阻塞

context.Context

包context 定义了 Context 类型,它在 API 边界和进程之间传递截止时间、取消信号和其他请求作用域值。

服务端的传入请求应该创建一个 Context,对服务端的外部调用应该接受一个 Context。它们之间的函数调用链必须传播 Context,并且可以使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 创建派生的 Context 来替换它。当一个 Context 被取消时,所有从它派生的 Context 也会被取消。

WithCancel、WithDeadline 和 WithTimeout 函数接受一个 Context(父级)并返回一个派生的 Context(子级)和一个 CancelFunc。调用 CancelFunc 会取消子级及其子级,移除父级对子级的引用并停止任何相关的定时器。如果不调用 CancelFunc,则会泄漏子级及其子级,直到父级被取消或定时器触发。go vet 工具检查所有控制流路径上是否使用了 CancelFuncs。

  1. 不要在结构类型中存储 Context;相反,将 Context 显式传递给需要它的每个函数(通常作为第一个参数,以ctx命名)
  2. 即使函数允许,也不要传递 nil Context。如果不确定要使用哪个 Context,请传递 context.TODO
  3. 仅将 context Value 用于跨进程和 API 传递的请求作用域数据,而不是将可选参数传递给函数
  4. 同一个 Context 可以传递给在不同 goroutine 中运行的函数;Context 可以同时被多个 goroutine 安全使用
go 复制代码
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{} 
    Err() error Value(key interface{}) interface{}
}
创建 context

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

这两个函数其实只是互为别名,没有差别,官方给的定义是:

  • context.Background 是上下文的默认值,通常作为根Context,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO 应该只在不确定应该使用哪种上下文时使用;
  • 在大多数情况下,我们都使用context.Background作为起始的上下文向下传递。
派生 context
scss 复制代码
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
  • WithCancel函数返回parent的副本并创建一个新的Done通道。当调用返回的cancel函数或父级上下文的Done通道关闭时,返回的上下文的Done通道关闭。
  • WithDeadline函数返回父Context的副本,其截止时间早于或等于d。如果父Context的截止时间早于d,则WithDeadline(parent, d)在语义上等同于parent。返回的Context的Done通道在到期时关闭,当调用返回的cancel函数时关闭,或者当父Context的Done通道关闭时关闭,以先发生的事件为准。
  • WithTimeout函数返回WithDeadline(parent, time.Now().Add(timeout))
  • WithValue函数返回parent的副本,其中与键关联的值为val。仅将context Values用于跨进程和API传递的请求范围数据,而不是将可选参数传递给函数。所提供的key 必须是可比较的,不应该是字符串或任何其他内置类型。
总结

context.Context主要是用来给父协goroutine来控制子goroutine、孙goroutine、...的。当父goroutine调用了其创建的context.Context的cancel函数,该Context所派生的所有Context的cancel函数都将被调用,所有的Done通道都将写入完成。通过这种机制实现父子关系的goroutine的协同,防止父goroutine退出,子goroutine未退出,出现协程泄露。

另外就是作为跨goroutine调用的执行上下文,可以在跨goroutine调用时传递信息。

sync包中的工具

sync.Mutex 与 sync.RWMutex

Mutex(互斥锁)

  • Mutex 为互斥锁,Lock() 加锁,Unlock() 解锁
  • 在一个 goroutine 获得 Mutex 后,其他 goroutine 只能等到这个 goroutine 释放该 Mutex
  • 使用 Lock() 加锁后,不能再继续对其加锁,直到利用 Unlock() 解锁后才能再加锁
  • 在 Lock() 之前使用 Unlock() 会导致 panic 异常
  • 已经锁定的 Mutex 并不与特定的 goroutine 相关联,这样可以利用一个 goroutine 对其加锁,再利用其他 goroutine 对其解锁
  • 在同一个 goroutine 中的 Mutex 解锁之前再次进行加锁,会导致死锁
  • 适用于读写不确定,并且只有一个读或者写的场景

RWMutex(读写锁)

  • RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁
  • 读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取读锁
  • 写锁会阻止其他 goroutine(无论读和写)进来,整个锁由该 goroutine 独占
  • 适用于读多写少的场景
sync.WaitGroup

sync.WaitGroup主要用来使主goroutine等待并确知执行任务的子goroutine都已经完成了。

go 复制代码
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()

子goroutine在执行任务前先调用Add(1),执行完成后调用Done(),主goroutine则调用Wait()等待所有的子任务都执行完毕。

  • Add 方法用于将计数器增加 delta,可以为正数或负数。当计数器大于零时,Wait() 方法会阻塞。一般情况下,我们会在启动新的 Goroutine 前调用 Add 方法,将需要等待的 Goroutine 数量添加到计数器中。
  • Done 方法会将计数器减一,相当于通知 WaitGroup 一个已完成的 Goroutine。一般情况下,我们会在 Goroutine 完成后调用 Done 方法。
  • Wait 方法会阻塞当前 Goroutine,直到计数器减为零,也就是所有被等待的 Goroutine 都已经完成执行。
sync.Once

sync.Once 是 Go 语言标准库中的一种同步原语,用于实现只执行一次的操作。

sync.Once 提供了一个 Do 方法,该方法接收一个函数作为参数,并保证该函数只会被执行一次,无论调用 Do 方法的次数有多少。在第一次调用 Do 方法时,传入的函数会被执行,而后续所有调用 Do 方法的操作都会被忽略。

sync.Cond
go 复制代码
func NewCond(l Locker) *Cond

其中,NewCond 是一个函数,用于创建并初始化一个新的 sync.Cond 对象。它接收一个实现了 Locker 接口(如 sync.Mutex 或 sync.RWMutex)的互斥锁作为参数,并返回一个指向新创建的 sync.Cond 对象的指针。

go 复制代码
func (c *Cond) Wait()

Wait 是 sync.Cond 类型的方法,用于在条件变量上等待通知。调用 Wait 方法会使当前 Goroutine 进入阻塞状态,直到收到其他 Goroutine 发送的通知信号。

go 复制代码
func (c *Cond) Signal()

Signal 是 sync.Cond 类型的方法,用于发送单个通知信号。调用 Signal 方法会唤醒正在等待中的至少一个 Goroutine,使其从 Wait 方法的阻塞状态返回继续执行。

go 复制代码
func (c *Cond) Broadcast()

Broadcast 是 sync.Cond 类型的方法,用于广播通知信号。调用 Broadcast 方法会唤醒所有等待中的 Goroutine,使它们从 Wait 方法的阻塞状态返回继续执行。

sync.Map 与 sync.Pool

sync.Map是原生map的并发安全版本

timer定时器

go 复制代码
import "time"

// 创建一个 Timer 对象,并设置定时器的持续时间为 duration。
// 当时间到达 duration 后,定时器将触发。
timer := time.NewTimer(duration)

// 通过从 timer.C 的通道中接收一个值,程序将阻塞在此处直到定时器触发或者定时器被停止
<-timer.C

// 调用 timer.Stop() 可以停止定时器。
// 如果定时器还没有触发,调用 Stop 方法将会使定时器无效,防止触发事件的发生。
// 已经触发的定时器无法再被停止。
timer.Stop()
相关推荐
java小吕布1 小时前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
Goboy1 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员
李少兄2 小时前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端
代码小鑫2 小时前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
Json____2 小时前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
monkey_meng2 小时前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss3 小时前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
大鲤余3 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
她说彩礼65万3 小时前
Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
后端·asp.net·mvc
WEIII3 小时前
MySQL 主从复制原理与搭建实践
后端·mysql·docker