Go の 竞争态
什么是竞态
如果两个或者两个以上的 goroutine
在没有相同同步的情况下,访问某个资源,并同时试图读写,就会出现竞争态,竞争态会让代码编写变得复杂,引起潜在危险。
例1
比如下面的例子,我们对a加了一百次,但是由于存在竞争问题,所以最后输出的结果是 88
func main() {
a = 0
for i := 0; i < 100; i++ {
wg.Add(1)
go f1()
}
wg.Wait()
fmt.Println(a)
}
func f1() {
defer wg.Done()
b := a
// 相当于从p队列中先踢出去,放到全局队列中
runtime.Gosched()
b++
a = b
}
// 输出
88
自动检测是否存在竞争态
可以通过执行这行代码,检测是否处在竞争态
go run -race main.go
锁住共享资源
对共享资源进行保护,最直观的方式,就把资源锁住,只有处理完了以后,其他的才能再继续进行。
其实对资源上锁,atomic
和 sync
包里的函数,都提供了很好的解决方案。
使用原子函数
atomic.AddInt64(&a,1) //安全+1
b=atomic.LoadInt64(&a) //安全读
atomic.StoreInt64(&a,12) //安全的写
使用原子函数,可以实现安全的读和安全的写,但是个人感觉这种方法局限性比较大。只能适用于特定的场合
使用互斥函数
使用互斥函数,就是相当于添加了一个临界区
var mutex01 sync.Mutex //互斥函数要声明
func f1() {
defer wg.Done()
runtime.Gosched()
// 相当于添加临界区
mutex01.Lock()
{
b := a
runtime.Gosched()
b++
a = b
}
mutex01.Unlock()
// 临界区结束
}
使用通道
通道有两种,一种是有缓冲的通道,一种是没有缓冲的通道,通道的声明格式如下:
unbuffchan := make(chan int) //无缓冲的通道
buffchan := make(chan int, 3) //有缓冲的通道
有缓冲的通道,和没有缓冲的通道,用法有些不同,应该根据实际情况,决定使用哪种通道
两种通道
无缓冲的通道
无缓冲的通道,是指没有能力保存任何值得通道。必须两头都得装备好才行,如果只有一头装备好了,那他就得阻塞,直到另一头也准备好。
func main() {
chan01 = make(chan int) //无缓冲的通道
wg.Add(1)
go func() {
defer wg.Done()
c := <-chan01
fmt.Println(c)
}()
chan01 <- 5
wg.Wait()
}
有换缓冲区的通道
有缓冲区的通道,就是在值被接受之前,可以存储一个或者多个值的通道。因此阻塞的条件和和无缓冲区的不一样,只有缓冲区满了,发送方才会阻塞,只有通道空了,接收方才会阻塞。
unbuffchan := make(chan int) //无缓冲的通道
buffchan := make(chan int, 3) //有缓冲的通道
如果接受者从一个永远不会接受到值得通道等着,那么就形成了死锁
如下面的例子,你往里放了五个值,非要取出来6个,那么这个第六个值永远不会等到
但是你发了,那边没人接收,这不会导致死锁
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go seeplyGoher(i, c)
}
for i := 0; i < 6; i++ {
gordid := <-c
fmt.Println(gordid)
}
}
func seeplyGoher(id int, c chan int) {
time.Sleep(time.Second * 5)
c <- id
fmt.Println("********", id, "******")
}
这个地方要特别注意一点,如果通道被阻塞,除了goroutien本身占用少量的内存外,被阻塞的goroutine并不会消耗其他资源。他就会静静的等在那里,等待导致其阻塞的事情,来解除阻塞。
超时处理
我们可以对 chan
进行超时处理,如果超过了时间,我们就不取了,这个功能可以使用 select
功能实现。
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
go sleepyGopher(i, c)
}
timeout := time.After(2 * time.Second) //以两秒为界限
for i := 0; i < 5; i++ {
select {
case gopid := <-c:
fmt.Println("能够按时取出的id", gopid)
case <-timeout:
fmt.Println("超时了")
}
}
}
func sleepyGopher(id int, c chan int) {
time.Sleep(time.Duration(rand.Intn(5)) * time.Second) //沉睡0-5秒不等的时间
c <- id //将10赋给id
}
注意一点:select在不包含任何case的情况下,将永远的等下去
nil通道
-
如果不实用
make
初始化通道,那么通道的值就是nil
零值 -
对
nil
通道进行发送或接受不会引起panic
,但是会导致永久阻塞 -
对
nil
通道执行close
函数,就引起阻塞 -
nil 通道的用处: 对于包含
select
语句的循环,如果不希望每次循环都等待select
所涉及的所有通道,那么可以先将这些通道置为nil
,等到发送值准备就绪以后,再将通道变成一个非nil
值并执行发送。
close()函数
-
通道在没有数写入的时候,就可以关闭了,我们可以通过
close
函数关闭通道 -
函数如果被关闭,就无法写入任何值,如果尝试写入,就会引发
panic
-
如果尝试读取一个已经关闭的通道,那么就会获得与通道类型相对应的零值。
-
注意: 如果循环中,读取一个已经关闭的通道,并没有检测通道是否已经关闭,那么这个循环就会一直空转下去,消耗大量的
cpu
资源。
地鼠装配线
对待比较复杂的问题,我们其实可以使用流水线装配的思想来完成;
具体的代码实现:
func F1(down chan int) {
for i := 0; i < 5; i++ {
down <- (i + 1)
}
close(down)
}
func F2(up, down chan int) { //进行乘以2的操作
for {
if a, ok := <-up; !ok {
close(down) //关闭他往下的通道
break
} else {
down <- a * a //求平方
}
}
}
func F3(up chan int) { //
for {
if a, ok := <-up; !ok {
break
} else {
fmt.Println(a)
}
}
}
func main() {
c0 := make(chan int)
c1 := make(chan int)
go F1(c0)
go F2(c0, c1)
go F3(c1)
time.Sleep(time.Second * 5) //如果下面不想等待,那么可以直接把最后一个 F3 前边的go删掉
}