Go-实战之 Go 并发中的坑


坑点

坑点 1:将 wg.add 放在 goroutine 中

请看下列代码

package main

import (
	"fmt"
	"sync"
)

func main() {
	for i := 0; i < 10; i++ {
		a := make([]int, 10)
		w := sync.WaitGroup{}
		for i := 0; i < 10; i++ {
			ii := i
			go func() {
				w.Add(1)
				a[ii] = 1
				w.Done()
			}()
		}
		w.Wait()
		fmt.Println(a)
	}
}

结果如下:

[1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 0]
[1 1 1 1 1 1 1 1 1 1]
[1 1 1 1 1 1 1 1 1 0]
[1 1 1 1 1 1 1 0 0 0]
[1 1 1 1 1 1 1 1 1 0]
[1 1 1 1 1 1 1 0 0 0]
[1 1 1 1 1 1 1 1 1 0]
[1 1 1 1 1 1 1 0 0 0]
[1 1 1 1 1 1 1 1 1 0]

不难发现,有许多 go func 都没有成功运行,因为在他们成功 add 之前就已经结束 wait 了。

坑点 2:互斥数据的锁

code1

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	ans1 = 0
	ans2 = 0
	ans3 = 0
	wg   sync.WaitGroup
)

func TestA(a int) {
	//fmt.Println("a:", a)
	defer wg.Done()
	ans1 = ans1 + a
}
func main() {
	for r := 0; r < 1000; r++ {
		ans1 = 0
		ans2 = 0
		ans3 = 0
		fmt.Println("第", r, "次测试")
		wg.Add(10)
		for i := 0; i < 10; i++ {
			go TestA(i)
		}
		wg.Wait()
		fmt.Println("ans1:", ans1)
		time.Sleep(time.Second)
		wg.Add(10)
		for i := 0; i < 10; i++ {
			go func() {
				fmt.Println("i:", i)
				ans2 = ans2 + i
				wg.Done()
			}()
		}
		wg.Wait()
		fmt.Println("ans2:", ans2)
		time.Sleep(time.Second)

		wg.Add(10)
		for i := 0; i < 10; i++ {
			ii := i
			go func() {
				//fmt.Println("ii:", ii)
				ans3 = ans3 + ii
				wg.Done()
			}()
		}
		wg.Wait()
		fmt.Println("ans3:", ans3)
		time.Sleep(time.Second)
		if ans1 != ans3 {
			fmt.Println("ans1 != ans3")
			break
		}
	}
}

需要特别注意三种数据传递方式,这里面第一种和第三种的数据传递是正确的?

不三个都是错误的,因为他们都并发操作了非并发安全的数据

而 ans2 还犯了使用 for 中数据作为传递的错误,这就导致了 ans2 的错误是最离谱的。

code2

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	ans1 = 0
	ans2 = 0
	ans3 = 0
	wg   sync.WaitGroup
)

func main() {
	for r := 0; r < 1000; r++ {
		ans1 = 0
		ans2 = 0
		ans3 = 0
		fmt.Println("第", r, "次测试")
		wg.Add(10)
		for i := 0; i < 10; i++ {
			go TestA(i)
		}
		wg.Wait()
		fmt.Println("ans1:", ans1)
		time.Sleep(time.Second)
		wg.Add(10)
		for i := 0; i < 10; i++ {
			ii := i
			go func() {
				ans3 = ans3 + ii
				wg.Done()
			}()
		}
		wg.Wait()
		fmt.Println("ans2:", ans3)
		time.Sleep(time.Second)
		if ans1 != ans3 {
			fmt.Println("ans1 != ans2")
			break
		}
	}
}

结论

  • ans1 的参数传递正确,但是 ans1 没有进行并发加锁,出现了脏写的情况
  • ans2 的参数引用是不可行的,而且也没有进行并发加锁,出现了 i 都是最后的 10,以及脏写的情况
  • ans3 的参数传递正确,但是 ans3 没有进行并发加锁,出现了脏写的情况

观察 code1 与 code2 两者的区别便在于两个输出语句,

我们恢复 code1 中 ans1 的输出语句即

func TestA(a int) {
	fmt.Println("a:", a)
	defer wg.Done()
	ans1 = ans1 + a
}

运行十次程序,均出错,出错原因均是 ans3 结果错误。同理,我们恢复 ans3 的输出语句,进行相同的模拟,出错原因均是 ans1 结果错误。

仔细分析后发现是没有对 ans 这些互斥数据进行加锁导致出现了脏写,而之所以加上输出后没有出错,可能是由于执行时间的差异导致数据成功存储。

加互斥锁之后可以成功通过千次测验。

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	ans1   = 0
	ans2   = 0
	ans3   = 0
	wg     sync.WaitGroup
	mutex1 = make(chan struct{}, 1)
	mutex3 = make(chan struct{}, 1)
)

func TestA(a int) {
	defer wg.Done()
	//申请锁
	mutex1 <- struct{}{}
	ans1 = ans1 + a
	//释放锁
	<-mutex1
}
func main() {
	for r := 0; r < 1000; r++ {
		ans1 = 0
		ans2 = 0
		ans3 = 0
		fmt.Println("第", r, "次测试")
		wg.Add(10)
		for i := 0; i < 10; i++ {
			go TestA(i)
		}
		wg.Wait()
		fmt.Println("ans1:", ans1)
		time.Sleep(time.Second)
		//wg.Add(10)
		//for i := 0; i < 10; i++ {
		//	go func() {
		//		ans2 = ans2 + i
		//		wg.Done()
		//	}()
		//}
		//wg.Wait()
		//fmt.Println("ans2:", ans2)
		//time.Sleep(time.Second)

		wg.Add(10)
		for i := 0; i < 10; i++ {
			ii := i
			go func() {
				//申请锁
				mutex3 <- struct{}{}
				ans3 = ans3 + ii
				wg.Done()
				//释放锁
				<-mutex3
			}()
		}
		wg.Wait()
		fmt.Println("ans3:", ans3)
		time.Sleep(time.Second)
		if ans1 != ans3 {
			fmt.Println("ans1 != ans3")
			break
		}
	}
}


如果本文帮助到了你,帮我点个广告可以咩(o′┏▽┓`o)


文章作者: Anubis
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Anubis !
评论
 上一篇
Go Slice 详解 Go Slice 详解
本文详细介绍了Go语言中切片(slice)的使用方法和原理,包括新建方法、函数操作、遍历、删除元素等,同时解释了切片扩容的机制。通过重点问题和注意点的讨论,帮助读者更好地理解和运用切片。如果你想掌握Go中切片的使用技巧和性能优化,这篇文章会对你有所帮助。
2023-07-21
下一篇 
windows 下目录末尾带空格 windows 下目录末尾带空格
啥都没写,别看
2023-06-11
  目录