Go控制并发
共享内存并发机制
在 go 中可以使用 sync.Mutex 或者 sync.RWMutex 来实现
sync.Mutex: 互斥锁
sync.RWMutex:一种特殊类型的锁,其允许多个只读操作并行执行,但写操作会完全互斥。
sync.WaitGroup:一种特殊的计数器,这个计数器需要在多个goroutine操作时做到安全并且提供提供在其减为零之前一直等待的一种方法。
package share_mem
import (
"sync"
"testing"
"time"
)
// 非线程安全
func TestCounter(t *testing.T) {
counter := 0
for i := 0; i < 5000; i++ {
go func() {
counter++
}()
}
time.Sleep(1 * time.Second)
t.Logf("counter = %d", counter)
}
// 线程安全
func TestCounterThreadSafe(t *testing.T) {
var mut sync.Mutex
counter := 0
for i := 0; i < 5000; i++ {
go func() {
// 释放锁的匿名函数
defer func() {
mut.Unlock()
}()
// 加锁
mut.Lock()
counter++
}()
}
time.Sleep(1 * time.Second) // 防止主协程运行过快,子协程还没有运行完就结束了
t.Logf("counter = %d", counter)
}
// 线程安全及等待子 goroutine 运行结束
func TestCounterWaitGroup(t *testing.T) {
var mut sync.Mutex // 用于控制counter在主和子协程中的竞争
var wg sync.WaitGroup // 用的等待子 goroutine 执行结束,相当于 python 中的join
counter := 0
for i := 0; i < 5000; i++ {
wg.Add(1) // +1
go func() {
defer func() {
mut.Unlock()
}()
mut.Lock()
counter++
wg.Done() // -1
}()
}
wg.Wait()
t.Logf("counter=%d", counter)
}
CSP并发
CSP(Communicating Sequential Processes 通信数据进程)是指依赖于通道(channel)完成通信实体(process)之间的协调
CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。
go 语言抽象出了 CSP 中的 channel 和 process(go 中对应 goroutine),底层实现并没有关系。
所以,凡是用到 goroutine 和 channel 实现的并发都可以归类到 CSP 并发
csp vs Actor
- 和 Actor 的直接通讯不同,CSP 模式则是通过 Channel 进行通讯的,更松耦合一些
- Go 中 channel 是有容量限制并且独立于处理 Goroutine;而如Erlang, Actor模式中的 mailbox 容量是无限的,接收进程也总是被动地处理消息。
异步返回结果
异步返回在 go 中可以用 goroutine + channel 实现,我们将原函数返回的结果放入 channel, 从 channel 中获取返回结果之前,我们可以再做一些其他事。
由下面的测试代码可以得出,buffer channel 更适合服务端开发。
package async_test
import (
"fmt"
"testing"
"time"
)
func service() string {
time.Sleep(time.Millisecond * 50)
return "Done"
}
func otherTask() {
fmt.Println("Working on something else")
time.Sleep(time.Millisecond * 100)
fmt.Println("Task is done.")
}
// 串行
func TestService(t *testing.T) {
fmt.Println(service())
otherTask()
}
// 异步执行
// 将 service 的结果放入 channel, 并将 channel 返回
func AsyncService() chan string {
// retCh := make(chan string) // 传值端和接收端都会卡住
retCh := make(chan string, 1) // buffer channel,和有限长度的队列是一个道理
// 开启一个goroutine
go func() {
ret := service()
fmt.Println("returned result.")
retCh <- ret
fmt.Println("service exited.")
}()
return retCh
}
func TestAsyncService(t *testing.T) {
retCh := AsyncService()
otherTask()
fmt.Println(<-retCh)
// time.Sleep(time.Second * 1) // 这里是为了让非缓存 channel 可以打印"service exited."
}
多路复用和超时控制
当我们需要从多个 channel 中接受消息时,就要用到 select 多路复用。
注意:如果多个 case 同时就绪时,select 会随机地选择一个执行,这样来保证每一个 channel 都有平等的被 select 的机会。
package async_test
import (
"fmt"
"testing"
"time"
)
func service() string {
time.Sleep(time.Millisecond * 50)
return "Done"
}
func otherTask() {
fmt.Println("Working on something else")
time.Sleep(time.Millisecond * 100)
fmt.Println("Task is done.")
}
// 异步执行
// 将 service 的结果放入 channel, 并将 channel 返回
func AsyncService() chan string {
// retCh := make(chan string) // 传值端和接收端都会卡住
retCh := make(chan string, 1) // buffer channel,和有限长度的队列是一个道理
// 开启一个goroutine
go func() {
ret := service()
fmt.Println("returned result.")
retCh <- ret
fmt.Println("service exited.")
}()
return retCh
}
func TestSelect(t *testing.T) {
select {
case ret := <-AsyncService():
t.Log(ret)
case <-time.After(time.Millisecond * 100):
t.Error("time out")
}
}
channel的关闭和广播
当我们使用一个生产者与多个消费者进行交互时,我们可以通过close(channel)来广播式的告诉所有的消费者去停止消费。
- 向关闭的 channel 发送数据,会导致 panic
- v,ok <-ch; ok 为 bool 值,true 表示正常接受,false 表示通道关闭
- 所有的 channel 接收者都会在 channel 关闭时,立刻从阻塞等待中返回上述 ok 值为 false。这个广播机制常被利用,进行向多个订阅者同时发送信号。如:退出信号。
package channel_close
import (
"fmt"
"sync"
"testing"
)
func dataProducer(ch chan int, wg *sync.WaitGroup) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
wg.Done()
}()
}
func dataReceiver(ch chan int, wg *sync.WaitGroup) {
go func() {
for {
if data, ok := <-ch; ok {
fmt.Println(data)
} else {
break
}
}
wg.Done()
}()
}
func TestChannel(t *testing.T) {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
dataProducer(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
wg.Wait()
}
任务的取消
如果我们开启很多 goroutine, 需要全部取消的时候可以有两种方式:通过共享变量取消、通过 select + channel 广播取消
package cancel_by_close
import (
"fmt"
"testing"
"time"
)
var cancelFlag bool
// 通过共享变量取消
func TestCancel1(t *testing.T) {
for i := 0; i < 5; i++ {
go func(i int) {
for {
if cancelFlag {
break
}
}
fmt.Println(i, "Cancelled")
}(i)
}
cancelFlag = true
time.Sleep(time.Millisecond * 10)
}
func isCancelled(cancelChan chan struct{}) bool {
select {
case <-cancelChan:
return true
default:
return false
}
}
func cancel1(cancelChan chan struct{}) {
cancelChan <- struct{}{}
}
func cancel2(cancelChan chan struct{}) {
close(cancelChan)
}
// 通过 select + channel 广播取消
func TestCancel(t *testing.T) {
cancelChan := make(chan struct{}, 0)
for i := 0; i < 5; i++ {
go func(i int, cancelCh chan struct{}) {
for {
if isCancelled(cancelCh) {
break
}
}
fmt.Println(i, "Cancelled")
}(i, cancelChan)
}
//cancel1(cancelChan) // 只会取消其中一个任务
cancel2(cancelChan) // 所有任务全部取消
time.Sleep(time.Millisecond * 10)
}
关联任务的取消
当我们启动的子任务又启动了孙子任务又启动了曾孙子任务这种关联任务的取消我们要用到context
- 根节点需要通过 context.Background() 创建
- 新的子节点需要用他的父节点创建,例根节点的子节点创建方式:ctx, cancel := context.WithCancel(context.Background())
- 当前 Context 被取消时,基于他的子 context 都会被取消
- 接收取消通知 <-ctx.Done()
package ctx_test
import (
"context"
"fmt"
"testing"
"time"
)
func isCancelled(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
func TestCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 5; i++ {
go func(i int, ctx context.Context) {
for {
if isCancelled(ctx) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctx)
}
cancel()
time.Sleep(time.Second * 1)
}