go语言面试题:goroutine、waitgroup、mutex

本文通过一道Go语言面试题,探讨了goroutine、waitgroup和mutex在并发编程中的应用。分析了并发写入map时可能出现的问题,强调了在并发环境下对非线程安全数据结构的操作需要同步机制。通过示例展示了不加锁和加锁情况下map的执行结果,解释了加锁的意义以及如何正确地向map中写入N对键值对。

点击个人博客,查看更多文章https://elonjelinek.github.io

判断程序的执行结果,并解释原因

package main

import (
	"fmt"
	"sync"
)

const N = 20

func main() {
	wg := sync.WaitGroup{}
	mu := sync.Mutex{}
	m := make(map[int]int)

	wg.Add(N)

	for i := 0; i < N; i++ {
		go func() {
			mu.Lock()
			defer wg.Done()
			m[i] = i
			mu.Unlock()
		}()
	}
	wg.Wait()
	fmt.Println(len(m))
	fmt.Println(m)
}

运行结果

3
map[12:13 13:13 20:20]

再次运行

5
map[15:16 20:20 10:12 12:12 13:13]

答案:结果不确定。原因:1、当N等于20的时候,主函数里启动了20个goroutine,并wait20个goroutine执行,只有当20个goroutine都执行完了以后,主函数才会结束,保证了map会被写20次。但是,这里虽然加了锁,加锁的原因却是map在并发中不是线程安全的,map不能并发的写,只可以并发的读取,所以这里不加锁就会报错。
打印i

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mg sync.WaitGroup
	var mu sync.Mutex
	const N = 20
	m := make(map[int]int)

	mg.Add(N)
	for i := 0; i < N; i++ {
		go func() {
			mu.Lock()
			m[i] = i
			fmt.Println(m[i], i)
			mu.Unlock()
			mg.Done()
		}()
	}
	mg.Wait()
}

运行结果

0 16
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20
20 20

多运行几次,结果不是每次都相同。

这里加锁只是保证在一个goroutine操作map的时候,另一个goroutine不能操作map,但是这里的i,对于所有的goroutine来说,相当于一个全局变量,其变化是由主函数控制的,所以虽然启动了20个goroutine,map被写了20次,但是很多个goroutine拿到的i是同一个数,所以map被很多次写操作都被覆盖了,每次写的key,value都一样,所以map里并没有20对键值对,当N很小的时候,map里只有一个键值对,但是即便N足够大,map里键值对的数量也少于N对儿。
并且还会发生map的key与value并不相等的情况,也就是map拿到的i是在变化的。
如果想要往map里写入N对键值对,那么就必须给匿名函数传参,这样,每启动一个goroutine,拿到的都是不同的值,所以map的key全都不一样,所以每一次写入的数据都不会被覆盖,这时,map中就有N对键值对。

下面不加等待,打印一下i,会发现这个打印顺序是不确定的,i的值是不确定的,key与value也会不一样,并且有些value还会被覆盖。

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	const N = 20
	m := make(map[int]int)
	for i := 0; i < N; i++ {
		go func() {
		mu.Lock()
		m[i] = i
		fmt.Println(m[i],i,"----------------")
		mu.Unlock()
		}()
	}
	fmt.Println(len(m))
	fmt.Println(len(m))
	fmt.Println(m)
	fmt.Println(len(m))
	fmt.Println(len(m))
}

运行结果

0 13 ----------------
1
20 20 ----------------
20 20 ----------------
20 20 ----------------
20 20 ----------------
2
20 20 ----------------
20 20 ----------------
20 20 ----------------
20 20 ----------------
20 20 ----------------
map[4:13 20:20]
2
2
20 20 ----------------
20 20 ----------------

观察上面的结果,会发现,第一次打印的value是个0,但是后面打出来的map中并没有一个键值对的value是0。
这里带横线的那一行最终打印了12次,但是有11次,拿到的i都是同一个数20,也就是说map至少被执行了12次写操作,但是最终map的长度为2,里面只有两对键值对,而且有一对键值对的key与value并不相等。说明第一次打印的时候,map里面还是空的,并没有被写入数据,但是数据i已经拿到了,是13,而13对应的key是4,所以证明了12个goroutine拿到的数据i是个主函数里面的共享数据,【对12个goroutine来说,i相当于是个全局变量】,大家抢的是同一个数,而在并发执行抢占资源的过程中,for循环一直在执行,所以先抢到的i会比较小,会有4和13,但是后抢到的i,全都是20。
所以,只有给匿名函数传参的时候,12个goroutine拿到的数据才不是共享数据,而是每一个goroutine拿到的都是单独的数据。
这里打印了四次map长度,第一次打印了1,原因是这个时候,有的goroutine还没有执行完,而最后两行带横线的输出,是因为有些goroutine没来得及打印输出,但是对map的写操作已经完成了,所以才在最后两行打印,而不是已经输出了map,打印了长度之后,又有两个goroutine才启动,又执行了两次写操作。

传参以后,map中就有了N对键值对

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	mu := sync.Mutex{}
	m := make(map[int]int)
	const  N  =20

	wg.Add(N)

	for i := 0; i < N; i++ {
		go func(n int) {
			defer wg.Done()
			mu.Lock()
			m[n] = n
			mu.Unlock()
		}(i)
	}
	wg.Wait()
	fmt.Println(len(m))
	fmt.Println(m)
}

运行结果

20
map[6:6 4:4 7:7 11:11 19:19 18:18 3:3 0:0 9:9 12:12 17:17 15:15 8:8 10:10 1:1 5:5 14:14 2:2 13:13 16:16]

点击个人博客,查看更多文章https://elonjelinek.github.io

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值