目录
问题引入
解决方法1:同步原语
同步原语无缓冲通道实战:两人网球比赛
同步原语无缓冲通道实战:模拟四人跑步接力
解决方法2:sync.Mutex互斥量
初始化顺序
参考
Go天生支持并发,也就是就算写成下面的样子的顺序:
go setup() {
fmt.Println("1")
} ()
go main() {
fmt.Println("2")
} ()
这里的setup和main执行顺序是无法预测的,因为在golang当中,这是两个并发的线程,在时间上是竞争关系。所以,在C/C++等语言中按照顺序执行操作这种很自然的操作在Go当中并不是很简单就能完成。
再看下面这个例子:
var done bool
func setup() {
fmt.Println("1")
done = true
}
func main() {
go setup()
for !done {} // 可能出现死循环
fmt.Println("2")
}
两个线程之间看似有时间上的执行先后顺序,但是因为两个线程之间没有同步事件,setup对done的写入有可能无法被main线程,所以main可能会陷入死循环!
利用Go语言中通道的的通信方式,具体的做法是创建通道,利用发送和接受共享资源,在goroutine之间做同步。
func main() {
// 创建无缓冲通道
done := make(chan int)
go func () {
println("hello word!")
done <- 1
} ()
<- done
}
在上面的例子当中,当 <- done 执行的时候,必然要求 done <- 1 也执行,这样可以保证顺序执行。
下面这张图很形象地说明了无缓冲通道之间的同步过程:
同步原理: 无缓冲通道是在接收前没有能力保存的通道,这种类型的通道在发送goroutine和接收gotoutine同时准备好才能完成发送和接收操作。所以无论是发送方还是接收方任意一方没有准备好,这时候都将处于阻塞状态,进行阻塞等待。
两人进行网球比赛,球会在两人之间传播,假定击球数为信号量在两个gorountine之间传输,这个是共享值。
选手只会处于两个状态:要么等待接球,要么将球击打给对方。只要任意一方没有准备好,都将处于阻塞状态,只能阻塞等待。
刚才描述的特征完全符合无缓冲通道的特征,所以用无缓冲通道进行模拟这个过程,以生成的100以内的随机数被13整除为一方击球失败的判断依据,代码如下:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
// 初始化随机数种子
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
// 创建无缓冲通道
court := make(chan int)
// gouroutine计数2
wg.Add(2)
// 无缓冲通道的时候,接收方没有能力保存任何值
// 只有当接收方的goroutine和发送方的gorountine都准备好的时候才能完成发送和接收操作
// 所以利用这一点做两个gorountine之间的同步和数据传输
go player("GaoLin", court)
go player("ZhengZhi", court)
// 发球
court <- 1
// 等待程序结束
wg.Wait()
}
// 模拟选手打网球
func player(name string, court chan int) {
// 函数运行结束通知main函数已经完成
defer wg.Done()
for {
// 等待球被打过来
ball, ok := <-court
if !ok {
// 如果通道被关闭,说明对方输了,则自己赢了
fmt.Printf("Player %s Won!\n", name)
return
}
// 取随机数,判断是否有丢球
n := rand.Intn(100)
// 当通道被13整除的时候判断为输球
if n%13 == 0 {
fmt.Printf("Player %s Missed!\n", name)
// 关闭通道并返回函数,表示已经输了
close(court)
return
}
// 显示击球数,击球数加一
fmt.Printf("Player %s Hit %d !\n", name, ball)
ball++
// 将球打回对手,利用通道传递击球数信息
court <- ball
}
}
运行效果:
接力棒是唯一的同步数据资源,四个跑步者围着赛道轮流进行接力,利用无缓冲通道的同步性质来模拟这一过程。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
// 创建通道传递当前接力者的信息
baton := make(chan int)
// 为最后一个跑步者将计数加一
wg.Add(1)
// 第一位跑步者开始持有接力棒
go Runner(baton)
// 开始比赛
baton <- 1
// 等待当前
wg.Wait()
}
func Runner(baton chan int) {
// 新接力者的信息
var newRunner int
// 获取通道内当前接力者的信息
runner := <-baton
// 开始接力跑步
fmt.Printf("Runner %d is running with baton!\n", runner)
// 比赛没有结束的时候创建下一位接力者
if runner != 4 {
newRunner = runner + 1
fmt.Printf("Runner %d is to the Line\n", newRunner)
go Runner(baton)
}
// 延迟,模拟跑步过程
time.Sleep(100 * time.Millisecond)
// 比赛结束
if runner == 4 {
fmt.Printf("Runner %d Finished, Game over!\n", runner)
// 返回函数并告知主函数已经比赛结束
wg.Done()
return
}
// 将接力棒传递给下一个接力者
fmt.Printf("Runner %d Exchange With Runner %d!\n", runner, newRunner)
// 利用通道传递当前接力棒接力者的信息
baton <- newRunner
}
运行效果:
func main() {
var mu sync.Mutex
mu.Lock()
go func() {
println("hello word!")
mu.Unlock()
} ()
// 此语句必然在mu.Unlock()之后执行,从而保证了顺序性
mu.Lock()
}
mu.Lock() 语句必然在mu.Unlock()之后发生,从而保证了顺序性。
一图概括,无需多言
首先从main包开始,如果main包中有import语句,则会导入这些包,如果要导入的这些包又有要导入的包,则继续先导入所依赖的包。重复的包只会导入一次,就像很多包都要导入fmt包一样,但它只会导入一次。
每个被导入的包在导入之后,都会先将包的可导出函数(大写字母开头)、包变量、包常量等声明并初始化完成,然后如果这个包中定义了init()函数,则自动调用init()函数。init()函数调用完成后,才回到导入者所在的包。同理,这个导入者所在包也一样的处理逻辑,声明并初始化包变量、包常量等,再调用init()函数(如果有的话),依次类推,直到回到main包,main包也将初始化包常量、包变量、函数,然后调用init()函数,调用完init()后,调用main函数,于是开始进入主程序的执行逻辑。
参考
https://www.cnblogs.com/f-ck-need-u/p/9847554.html
《Go语言高级编程》
《Go语言实战》