Go语言——顺序一致性与初始化顺序

目录

问题引入

解决方法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可能会陷入死循环!

解决方法1:同步原语

利用Go语言中通道的的通信方式,具体的做法是创建通道,利用发送和接受共享资源,在goroutine之间做同步。

func main() {
    // 创建无缓冲通道
    done := make(chan int)
    
    go func () {
        println("hello word!")
        done <- 1
    } ()
    
    <- done
}

 在上面的例子当中,当 <- done 执行的时候,必然要求 done <- 1 也执行,这样可以保证顺序执行。

下面这张图很形象地说明了无缓冲通道之间的同步过程:

Go语言——顺序一致性与初始化顺序_第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
	}
}

运行效果:

Go语言——顺序一致性与初始化顺序_第2张图片

同步原语无缓冲通道实战:模拟四人跑步接力

接力棒是唯一的同步数据资源,四个跑步者围着赛道轮流进行接力,利用无缓冲通道的同步性质来模拟这一过程。

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
}

运行效果:

Go语言——顺序一致性与初始化顺序_第3张图片

解决方法2:sync.Mutex互斥量

func main() {
    var mu sync.Mutex
    
    mu.Lock()
    go func() {
        println("hello word!")
        mu.Unlock()
    } ()
    
    // 此语句必然在mu.Unlock()之后执行,从而保证了顺序性
    mu.Lock()
}

mu.Lock() 语句必然在mu.Unlock()之后发生,从而保证了顺序性。

初始化顺序

一图概括,无需多言

Go语言——顺序一致性与初始化顺序_第4张图片

首先从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语言实战》

你可能感兴趣的:(Golang)