在Go中什么是竞争状态:如果两个或者多个goroutine在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)
2、如何消除竞争状态Go语言提供了传统的同步goroutine的机制,就是对共享资源加锁。
Go语言中有三种方式对共享资源加锁:
1、原子函数
2、互斥锁
3、通道
原子函数和互斥锁都能工作,但依靠他们都不会让编写并发程序变的更简单,更不容易出错,或者更有趣。在Go语言中,不仅可以使用原子函数和互斥锁来保证对共享资源的安全访问以及消除竞争状态,还可以使用通道,通过发送和接收需要共享的资源,在goroutine之间做同步。
当一个资源需要在goroutine之间共享时,通道在goroutine之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
3.1 通道的类型 1、无缓冲的通道无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通
道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收 *** 作。
注意:如果两个 goroutine没有同时准备好,通道会导致先执行发送或接收 *** 作的 goroutine 阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个 *** 作都无法离开另一个 *** 作单独存在。
如图:展示两个 goroutine 如何利用无缓冲的通道来共享一个值
在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收。在第 2 步,左侧
的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在
通道中被锁住,直到交换完成。在第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通
道里接收数据。这个 goroutine 一样也会在通道中被锁住,直到交换完成。在第 4 步和第 5 步,
进行交换,并最终,在第 6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住
的 goroutine 得到释放。两个 goroutine 现在都可以去做别的事情了。
代码案例:在网球比赛中,两位选手会把球在两个人之间来回传递。选手总是处在以下两种状态之一:
要么在等待接球,要么将球打向对方。可以使用两个 goroutine 来模拟网球比赛,并使用无缓冲
的通道来模拟球的来回。
// 这个示例程序展示如何用无缓冲的通道来模拟2个goroutine间的网球比赛
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// wg用来等待程序结束
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().UnixNano())
}
// main是所有Go程序的入口
func main() {
// 创建一个无缓冲的通道
court := make(chan int)
// 计数加2,表示要等待两个goroutine
wg.Add(2)
// 启动两个选手
go player("xiaobai01", court)
go player("xiaobai02", court)
// 发球
court <- 1
// 等待游戏结束
wg.Wait()
}
// player模拟一个选手在打网球
func player(name string, court chan int) {
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done()
for {
// 等待球被击打过来
ball, ok := <-court
if !ok {
// 如果通道被关闭,我们就赢了
fmt.Printf("Player %s Won\n", name)
return
}
// 选随机数,然后用这个数来判断我们是否丢球
n := rand.Intn(100)
if n%13 == 0 {
fmt.Printf("Player %s Missed\n", name)
// 关闭通道,表示我们输了
close(court)
return
}
// 显示击球数,并将击球数加1
fmt.Printf("Player %s Hit %d\n", name, ball)
ball++
// 将球打向对手
court <- ball
}
}
执行结果:
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类
型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。
注意: 通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
如图:看到两个goroutine分别向有缓冲的通道里增加一个值和从有缓冲的通道里移
除一个值。
在第 1 步,右侧的 goroutine 正在从通道接收一个值。在第 2 步,右侧的这个 goroutine
独立完成了接收值的动作,而左侧的 goroutine 正在发送一个新值到通道里。在第 3 步,左侧的
goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的
两个 *** 作既不是同步的,也不会互相阻塞。最后,在第 4 步,所有的发送和接收都完成,而通道
里还有几个值,也有一些空间可以存更多的值。
代码案例:管理一组 goroutine 来接收并完成工作。
// 这个示例程序展示如何使用由缓冲的通道和固定数目的goroutine来处理一堆工作
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
const (
numberGoroutines = 4 // 要使用的goroutine的数量
taskLoad = 10 // 要处理的工作的数量
)
// wg用来等待程序完成
var wg sync.WaitGroup
// init初始化包,Go语言运行时会在其他代码执行之前,优先执行这个函数
func init() {
// 初始化随机数种子
rand.Seed(time.Now().Unix())
}
// main是所有Go程序的入口
func main() {
// 创建一个有缓冲的通道来管理工作
tasks := make(chan string, taskLoad)
// 启动goroutine来处理工作
wg.Add(numberGoroutines)
for gr := 1; gr <= numberGoroutines; gr++ {
go worker(tasks, gr)
}
// 增加一组要完成的工作
for post := 1; post <= taskLoad; post++ {
tasks <- fmt.Sprintf("Task : %d", post)
}
// 当所有工作都处理完成时关闭通道,以便所有goroutine退出
close(tasks)
// 等待所有工作完成
wg.Wait()
}
// worker作为goroutine启动来处理
// 从有缓冲的通道传入的工作
func worker(tasks chan string, worker int) {
// 通知函数已经返回
defer wg.Done()
for {
// 等待分配工作
task, ok := <-tasks
if !ok {
// 这意味着通道已经空了,并且已被关闭
fmt.Printf("Worker: %d : Shutting Down\n", worker)
return
}
// 显示我们开始工作了
fmt.Printf("Worker: %d : Started %s\n", worker, task)
// 随机等一段时间来模拟工作
sleep := rand.Int63n(100)
time.Sleep(time.Duration(sleep) * time.Millisecond)
// 显示我们完成了工作
fmt.Printf("Worker: %d : Completed %s\n", worker, task)
}
}
执行结果:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)