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

锁住共享资源

对共享资源进行保护,最直观的方式,就把资源锁住,只有处理完了以后,其他的才能再继续进行。

其实对资源上锁,atomicsync 包里的函数,都提供了很好的解决方案。

使用原子函数

    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删掉
}
posted @ 2021-07-24 18:34  沧海一声笑rush  阅读(46)  评论(0编辑  收藏  举报