什么是并发同步?
并发同步是指如何控制若干并发计算(在Go中,即协程),从而
- 避免在它们之间产生数据竞争的现象;
- 避免在它们无所事事的时候消耗CPU资源。
并发同步有时候也称为数据同步。
Go支持的并发同步技术
通道
不要让计算通过共享内存来通讯,而应该让它们通过通讯来共享内存。
通道类型和值
- 每个通道类型也有一个元素类型。 一个通道只能传送它的(通道类型的)元素类型的值。
- 字面形式
chan T
表示一个元素类型为T
的双向通道类型。 编译器允许从此类型的值中接收和向此类型的值中发送数据。 - 字面形式
chan<- T
表示一个元素类型为T
的单向发送通道类型。 - 字面形式
<-chan T
表示一个元素类型为T
的单向接收通道类型。 - 双向通道
chan T
的值可以被隐式转换为单向通道类型chan<- T
和<-chan T
,但反之不行(即使显式也不行)。 类型chan<- T
和<-chan T
的值也不能相互转换。 - 一个容量为0的通道值称为一个非缓冲通道(unbuffered channel),一个容量不为0的通道值称为一个缓冲通道(buffered channel)。
- 一个非零通道值必须通过内置的
make
函数来创建。
通道的比较
所有通道类型均为可比较类型。
通道操作
关闭通道
//不能关闭已经关闭的通道,否则会恐慌 //不能关闭一个零值nil通道 close(ch)
关闭过程:
关闭channel时会把recvq中的G全部唤醒,写入数据为元素类型的零值;==把sendq中的G全部唤醒,但这些G会panic。(向已关闭的通道写入数据)==向通道发送值
//此通道值不能为单向接收的 ch <- v //向零值通道发送数据会永久阻塞 //向已关闭通道发送数据会产生恐慌
发送过程:
如果recvq不为空,则可知缓冲区无数据或者无缓冲区,此时取出G(从recvq取出),向其写入数据,唤醒G,结束发送过程;若recvq为空,并且缓冲区有空位,则直接向其写入,结束发送过程;无空位,则将数据写入G,将G加入到sendq,进入睡眠,等待被唤醒从通道接收值
//ch不能为单向发送通道 v = <-ch //第二个值是布尔值 //此类型不确定的布尔值表示第一个返回值是否是在通道被关闭之前被发送的。 v, sentBeforeClosed = <-ch //从零值通道接收数据会永久阻塞 //从已关闭的通道接收数据不会阻塞,若通道缓冲区中还有数据则依次收到数据,布尔值为true;没有数据则收到元素类型的零值,布尔值为false
接收过程:
若sendq不为空,且没有缓冲区,直接从sendq取出G,把G中数据读出,唤醒G,结束读取进程;有缓冲区,则此时缓冲区已满,从缓冲区首部读出数据,此时G中数据写入缓冲区尾部,唤醒G,结束进程;若sendq为空,缓冲区有数据,则取出数据,否则将其加入recvq,进入睡眠,等待唤醒查询通道容量、长度
cap(ch) //长度是指当前有多少个已被发送到此通道但还未被接收出去的元素值。 len(ch)
select-case
- 每个
case
关键字后必须跟随一个通道接收数据操作或者一个通道发送数据操作。 通道接收数据操作可以做为源值出现在一条简单赋值语句中。 - 所有的非阻塞
case
操作中将有一个被随机选择执行,然后执行此操作对应的case
分支代码块。 - 在所有的
case
操作均为阻塞的情况下,如果default
分支存在,则default
分支代码块将得到执行; 否则,当前协程将被推入所有阻塞操作中相关的通道的发送数据协程队列或者接收数据协程队列中,并进入阻塞状态。
Future与Promise
Future
和Promise
是其他语言(如JavaScript、Java等)中常见的异步编程模型,用来表示某个操作在未来某个时间点完成,并允许对其结果进行处理。Future指一个只读的值的容器,这个值可能立即可用,也可能在未来某个时间可用。而Promise则是一个只能写入一次的对象。每个Promise关联一个Future,对Promise的写入会令Future的值可用。
- Futures:表示一个异步操作的最终结果。
- Promises:用于设置Futures的值。
尽管Go语言没有内置的Future
或Promise
,但通道可以被用来实现类似的功能。
package main
import (
"fmt"
"time"
)
// 模拟一个耗时操作
func performTask() <-chan int {
result := make(chan int)
go func() {
// 假设这个操作需要2秒钟
time.Sleep(2 * time.Second)
result <- 42 // 结果为42
close(result)
}()
return result
}
func main() {
// 启动异步任务
future := performTask()
// 做一些其他工作
fmt.Println("Doing other work...")
// 等待异步任务的结果
result := <-future
fmt.Printf("Result is %d\n", result)
}
sync.WaitGroup
每个sync.WaitGroup
值在内部维护着一个计数,此计数的初始默认值为零。
func main() {
var wg sync.WaitGroup
wg.Add(2) //设置计数器,数值即为goroutine的个数
go func() {
//Do some work
time.Sleep(1*time.Second)
fmt.Println("Goroutine 1 finished!")
wg.Done() //goroutine执行结束后将计数器减1
}()
go func() {
//Do some work
time.Sleep(2*time.Second)
fmt.Println("Goroutine 2 finished!")
wg.Done() //goroutine执行结束后将计数器减1
}()
wg.Wait() //主goroutine阻塞等待计数器变为0
fmt.Printf("All Goroutine finished!")
}
Add()
设置的值必须与实际goroutine个数一致,否则panic- 计数器维护的数不能为负,否则panic
Add()
早于Wait()
,否则panic- 方法调用
wg.Done()
和wg.Add(-1)
是完全等价的。
sync.Once
每个*sync.Once
值有一个Do(f func())
方法。 此方法只有一个类型为func()
的参数。一个sync.Once
值被用来确保一段代码在一个并发程序中被执行且仅被执行一次。
func main() {
log.SetFlags(0)
x := 0
doSomething := func() {
x++
log.Println("Hello")
}
var wg sync.WaitGroup
var once sync.Once
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
//Hello将仅被输出一次,而world!将被输出5次
once.Do(doSomething)
log.Println("world!")
}()
}
wg.Wait()
log.Println("x =", x) // x = 1
}
sync.Mutex
和sync.RWMutex
type Mutex struct {
state int32
sema uint32
}
- 都实现了
sync.Locker
接口类型。 所以这两个类型都有两个方法:Lock()
和Unlock()
,用来保护一份数据不会被多个使用者同时读取和修改。 - 一个锁的加锁者和此锁的解锁者可以不是同一个协程
waiterCount
:等待锁的协程数starving
:是否饿死,在正常模式下,锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁,为了减少这种情况的出现,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式(优先唤醒队列中的等待者,新进入的线程将加入等待队列),防止部分 Goroutine 被饿死。正常模式下,所有等待锁的 goroutine 按照 FIFO(先进先出)顺序等待。但是协程如果加锁不成功不会立即转入等待队列,而是判断是否满足自旋的条件,如果满足则会自旋。
当持有锁的协程释放锁的时候,会释放一个信号量来唤醒等待队列中的一个协程,但如果有协程正处于自旋过程中,锁往往会被该自旋协程获取到。新请求的 goroutine 更容易抢占:因为它正在 CPU 上执行,所以刚刚唤醒的 goroutine有很大可能在锁竞争中失败。在这种情况下,这个被唤醒的 goroutine 会加入 到等待队列的前面。type RWMutex struct { w Mutex//只能有一个写锁 writerSem uint32//写阻塞等待的信号量 readerSem uint32//读阻塞等待的信号量 readerCount int32//读者个数 readerWait int32//写阻塞时的读者数量,为0时唤醒写操作 }
*sync.RWMutex
类型还有两个另外的方法:RLock()
和RUnlock()
,用来支持多个读取者并发读取一份数据但防止此份数据被某个数据写入者同时使用。- 获取写锁时会将
readCount-2^30
,读操作进来时发现readCount
小于0则阻塞,readCount
++ rwm
的写锁只有在它的写锁和读锁都处于未加锁状态时才能被成功加锁。- 当
rwm
的写锁正处于加锁状态的时候,任何新的对之加写锁或者加读锁的操作试图都将导致当前协程进入阻塞状态,直到此写锁被解锁。 - 假设
rwm
的读锁正处于加锁状态的时候,为了防止后续数据写入者没有机会成功加写锁,后续发生在某个被阻塞的加写锁操作试图之后的所有加读锁的试图都将被阻塞。 - 假设
rwm
的写锁正处于加锁状态的时候,(至少对于标准编译器来说,)为了防止后续数据读取者没有机会成功加读锁,发生在此写锁下一次被解锁之前的所有加读锁的试图都将在此写锁下一次被解锁之后肯定取得成功,即使所有这些加读锁的试图发生在一些仍被阻塞的加写锁的试图之后。
sync.Cond
每个sync.Cond
值拥有一个sync.Locker
类型的名为L
的字段。 此字段的具体值常常为一个*sync.Mutex
值或者*sync.RWMutex
值。*sync.Cond
类型有三个方法:Wait()
、Signal()
和Broadcast()
。
每个Cond
值维护着一个先进先出等待协程队列。 对于一个可寻址的Cond
值c
:
c.Wait()
必须在c.L
字段值的锁处于加锁状态的时候调用;否则,c.Wait()
调用将造成一个恐慌。 一个c.Wait()
调用将- 首先将当前协程推入到
c
所维护的等待协程队列; - 然后调用
c.L.Unlock()
对c.L
的锁解锁; - 然后使当前协程进入阻塞状态;当前协程将被另一个协程通过
c.Signal()
或c.Broadcast()
调用唤醒而重新进入运行状态。一旦当前协程重新进入运行状态,c.L.Lock()
将被调用以试图重新对c.L
字段值的锁加锁。 此c.Wait()
调用将在此试图成功之后退出。
- 首先将当前协程推入到
- 一个
c.Signal()
调用将唤醒并移除c
所维护的等待协程队列中的第一个协程(如果此队列不为空的话)。 - 一个
c.Broadcast()
调用将唤醒并移除c
所维护的等待协程队列中的所有协程(如果此队列不为空的话)。
原子操作
在 Go 语言中,可以使用 sync/atomic
包来实现原子操作。sync/atomic
包提供了一系列原子操作函数,可以在并发环境中安全地对共享变量进行读取、写入和其他原子操作,而无需显式地使用锁,常常直接通过CPU指令直接实现。
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var value int32
atomic.StoreInt32(&value, 10) // 原子写入值
newValue := atomic.LoadInt32(&value) // 原子读取值
fmt.Println("Original value:", newValue)
delta := int32(5)
newValue = atomic.AddInt32(&value, delta) // 原子增加值
fmt.Println("New value after addition:", newValue)
swapped := atomic.CompareAndSwapInt32(&value, 15, 20) // 原子比较并替换值
fmt.Println("Swapped:", swapped)
fmt.Println("Final value:", atomic.LoadInt32(&value))
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。