gitalk icon indicating copy to clipboard operation
gitalk copied to clipboard

Go并发编程小测验: 你能答对几道题?

Open smallnest opened this issue 6 years ago • 43 comments

https://colobu.com/2019/04/28/go-concurrency-quizzes/

smallnest avatar Apr 28 '19 11:04 smallnest

  1. D
  2. D
  3. D
  4. B
  5. D
  6. 不熟悉Pool,不知道……
  7. C
  8. D
  9. A
  10. B

另:求正确答案……

PureWhiteWu avatar Apr 29 '19 03:04 PureWhiteWu

正确答案加上了,回答的还不错。

@PureWhiteWu

  1. D
  2. D
  3. D
  4. B
  5. D
  6. 不熟悉Pool,不知道……
  7. C
  8. D
  9. A
  10. B

另:求正确答案……

smallnest avatar Apr 29 '19 03:04 smallnest

鸟哥求教一下,为什么第四题是C呢? @smallnest

PureWhiteWu avatar Apr 29 '19 03:04 PureWhiteWu

我感觉因为有mutex的保护,所以f一定只会被执行一次

PureWhiteWu avatar Apr 29 '19 03:04 PureWhiteWu

因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

smallnest avatar Apr 29 '19 03:04 smallnest

有道理,之前只考虑了IA64平台上的情况,在其它平台上不能保证没问题,多谢!

@smallnest 因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

PureWhiteWu avatar Apr 29 '19 04:04 PureWhiteWu

IA64情况下也是有问题的,因为Lock并没有保护着第13行的读,所以即使本goroutine设置了done =1,其它的goroutine在这个时候还是可能读取到done == 0,所以必须通过atomic保证happen before关系(atomic基本可以认为保证happen before,虽然官方没有明确保证,但是认为目前使用起来还没遇到过问题)

@PureWhiteWu 有道理,之前只考虑了IA64平台上的情况,在其它平台上不能保证没问题,多谢!

@smallnest 因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

smallnest avatar Apr 29 '19 07:04 smallnest

我认为在这里问题并不是happens before,而是缓存一致性。 Lock可以保证拿到锁的goroutine的write一定happens before 其它goroutine的19行的read。 但是由于其它goroutine在13行有读过done,所以可能会存在cpu的缓存中,导致19行中read是从CPU缓存读取的,就会导致脏读。

@smallnest IA64情况下也是有问题的,因为Lock并没有保护着第13行的读,所以即使本goroutine设置了done =1,其它的goroutine在这个时候还是可能读取到done == 0,所以必须通过atomic保证happen before关系(atomic基本可以认为保证happen before,虽然官方没有明确保证,但是认为目前使用起来还没遇到过问题)

@PureWhiteWu 有道理,之前只考虑了IA64平台上的情况,在其它平台上不能保证没问题,多谢!

@smallnest 因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

PureWhiteWu avatar Apr 29 '19 08:04 PureWhiteWu

你的理解是对的,通过对第13行,第19行使用atomic,可以给它们之间建立一个happen before的关系,保证写对读可见,否则也可以把第13行放在Lock的临界区中,但是性能就不保证了。

Go的内存模型就是定义happen before,保证某一个时刻的w,确保r能读取到。

@PureWhiteWu 我认为在这里问题并不是happens before,而是缓存一致性。 Lock可以保证拿到锁的goroutine的write一定happens before 其它goroutine的19行的read。 但是由于其它goroutine在13行有读过done,所以可能会存在cpu的缓存中,导致19行中read是从CPU缓存读取的,就会导致脏读。

@smallnest IA64情况下也是有问题的,因为Lock并没有保护着第13行的读,所以即使本goroutine设置了done =1,其它的goroutine在这个时候还是可能读取到done == 0,所以必须通过atomic保证happen before关系(atomic基本可以认为保证happen before,虽然官方没有明确保证,但是认为目前使用起来还没遇到过问题)

@PureWhiteWu 有道理,之前只考虑了IA64平台上的情况,在其它平台上不能保证没问题,多谢!

@smallnest 因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

smallnest avatar Apr 29 '19 08:04 smallnest

感谢大佬抽空指点! 经常看大佬博客,受益匪浅!

@smallnest 你的理解是对的,通过对第13行,第19行使用atomic,可以给它们之间建立一个happen before的关系,保证写对读可见,否则也可以把第13行放在Lock的临界区中,但是性能就不保证了。

Go的内存模型就是定义happen before,保证某一个时刻的w,确保r能读取到。

@PureWhiteWu 我认为在这里问题并不是happens before,而是缓存一致性。 Lock可以保证拿到锁的goroutine的write一定happens before 其它goroutine的19行的read。 但是由于其它goroutine在13行有读过done,所以可能会存在cpu的缓存中,导致19行中read是从CPU缓存读取的,就会导致脏读。

@smallnest IA64情况下也是有问题的,因为Lock并没有保护着第13行的读,所以即使本goroutine设置了done =1,其它的goroutine在这个时候还是可能读取到done == 0,所以必须通过atomic保证happen before关系(atomic基本可以认为保证happen before,虽然官方没有明确保证,但是认为目前使用起来还没遇到过问题)

@PureWhiteWu 有道理,之前只考虑了IA64平台上的情况,在其它平台上不能保证没问题,多谢!

@smallnest 因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

PureWhiteWu avatar Apr 29 '19 08:04 PureWhiteWu

@smallnest 能帮我解读一下第2题吗? mu.Lock()阻塞我能理解, 为什么第二个mu.RLock()也会阻塞?

meilihao avatar Apr 29 '19 08:04 meilihao

@meilihao @smallnest 能帮我解读一下第2题吗? mu.Lock()阻塞我能理解, 为什么第二个mu.RLock()也会阻塞?

因为go为了防止写锁饥饿,当有写锁等待时,后来的读锁获取不了,会等待写锁完成后再获取读锁。

PureWhiteWu avatar Apr 29 '19 08:04 PureWhiteWu

@smallnest 因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

是因为多核cpu缓存不一致吗?

LooJee avatar Apr 30 '19 01:04 LooJee

第四个,都加锁了,当然可以保证happen before 了。 应该选B

wshlovercn avatar Apr 30 '19 09:04 wshlovercn

鸟哥,@smallnest 第6题Pool的我跑的结果是这样的,不应该是内存先涨再回收吗:

Cycle 0: 0 MB
Cycle 1: 256 MB
Cycle 2: 513 MB
Cycle 3: 769 MB
Cycle 4: 1281 MB
Cycle 5: 1281 MB
Cycle 6: 1281 MB
Cycle 7: 1537 MB
Cycle 8: 1793 MB
Cycle 9: 2049 MB
Cycle 10: 2049 MB
......
Cycle 107: 14593 MB
Cycle 108: 15105 MB
Cycle 109: 2304 MB
Cycle 110: 0 MB
Cycle 111: 256 MB
Cycle 112: 513 MB
......

Shitaibin avatar May 03 '19 06:05 Shitaibin

@smallnest 你的理解是对的,通过对第13行,第19行使用atomic,可以给它们之间建立一个happen before的关系,保证写对读可见,否则也可以把第13行放在Lock的临界区中,但是性能就不保证了。

Go的内存模型就是定义happen before,保证某一个时刻的w,确保r能读取到。

@PureWhiteWu 我认为在这里问题并不是happens before,而是缓存一致性。 Lock可以保证拿到锁的goroutine的write一定happens before 其它goroutine的19行的read。 但是由于其它goroutine在13行有读过done,所以可能会存在cpu的缓存中,导致19行中read是从CPU缓存读取的,就会导致脏读。

@smallnest IA64情况下也是有问题的,因为Lock并没有保护着第13行的读,所以即使本goroutine设置了done =1,其它的goroutine在这个时候还是可能读取到done == 0,所以必须通过atomic保证happen before关系(atomic基本可以认为保证happen before,虽然官方没有明确保证,但是认为目前使用起来还没遇到过问题)

@PureWhiteWu 有道理,之前只考虑了IA64平台上的情况,在其它平台上不能保证没问题,多谢!

@smallnest 因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

这个并没有缓存一致性的问题,17 行以后的代码都是加锁的,数据肯定是一致的。
Java 加 volatile 是为了禁止指令重排,和这儿没关系。对比官方实现加 atomic 是因为 13 行没有加锁,所以为了原子操作 o.done 才加 atomic,不然会有 data race。
所以这个实现,我觉得虽然有数据竞争,但是 f 肯定也只执行一次。

zeast avatar May 07 '19 09:05 zeast

我觉得你说的有道理。

我应该把这个例子再设置复杂一点,不使用基本数据类型uint32,而是使用一个复杂的对象。 本意是测试java单例相同的问题:即使字段被赋值了,其他的goroutine也可能得到一个未完全初始化的对象

@zeast

@smallnest 你的理解是对的,通过对第13行,第19行使用atomic,可以给它们之间建立一个happen before的关系,保证写对读可见,否则也可以把第13行放在Lock的临界区中,但是性能就不保证了。

Go的内存模型就是定义happen before,保证某一个时刻的w,确保r能读取到。

@PureWhiteWu 我认为在这里问题并不是happens before,而是缓存一致性。 Lock可以保证拿到锁的goroutine的write一定happens before 其它goroutine的19行的read。 但是由于其它goroutine在13行有读过done,所以可能会存在cpu的缓存中,导致19行中read是从CPU缓存读取的,就会导致脏读。

@smallnest IA64情况下也是有问题的,因为Lock并没有保护着第13行的读,所以即使本goroutine设置了done =1,其它的goroutine在这个时候还是可能读取到done == 0,所以必须通过atomic保证happen before关系(atomic基本可以认为保证happen before,虽然官方没有明确保证,但是认为目前使用起来还没遇到过问题)

@PureWhiteWu 有道理,之前只考虑了IA64平台上的情况,在其它平台上不能保证没问题,多谢!

@smallnest 因为o.Done没有保护,通过atomic可以保证多CPU情况下不会有问题。 类似的Java的双检查要求变量使用volatile声明。可以和Once的实现代码对照

@PureWhiteWu 鸟哥求教一下,为什么第四题是C呢? @smallnest

这个并没有缓存一致性的问题,17 行以后的代码都是加锁的,数据肯定是一致的。
Java 加 volatile 是为了禁止指令重排,和这儿没关系。对比官方实现加 atomic 是因为 13 行没有加锁,所以为了原子操作 o.done 才加 atomic,不然会有 data race。
所以这个实现,我觉得虽然有数据竞争,但是 f 肯定也只执行一次。

smallnest avatar May 08 '19 04:05 smallnest

https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/golang-nuts/6HlK0Lz2O5Y 结论是,第四题中虽然没有正确实现单例,但是f也最多只能运行一次,而不会运行多次。

PureWhiteWu avatar May 09 '19 03:05 PureWhiteWu

是的,避免误导,答案C我修改了。

目前看来第四题可能存在几个问题:

  1. 非原子操作,在某些架构下是有问题的
  2. 先于f执行标志设置
  3. 有可能一个goroutine看到字段已经被赋值,但是它的值还没有初始化: https://groups.google.com/forum/#!topic/golang-nuts/816FGr-mTUs

代码库中也修改了一下,使用复杂的struct代替基本数据类型int

@PureWhiteWu https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!msg/golang-nuts/6HlK0Lz2O5Y 结论是,第四题中虽然没有正确实现单例,但是f也最多只能运行一次,而不会运行多次。

smallnest avatar May 09 '19 04:05 smallnest

go从哪个版本开始var c chan int可以直接使用,而不是像我最接触的c:=make(chan int,1)? 谢谢

nkbai avatar May 14 '19 10:05 nkbai

go从哪个版本开始var c chan int可以直接使用,而不是像我最接触的c:=make(chan int,1)? 谢谢

这个是声明一个channel,没有为它分配内存,要使用它还是需要make的,看下面的闭包函数。

LooJee avatar May 15 '19 01:05 LooJee

func TestC(t *testing.T) {
	var c chan int
	go func() {
		<-c
		t.Logf("read finished")
	}()
	c <- 1
	t.Logf("write finished")
}

这种做法,读写都会卡主,而不会崩溃,

nkbai avatar May 16 '19 03:05 nkbai

鸟哥,最后3题能给个分析么,没搞明白是为什么.....

cxzgb123 avatar Jun 21 '19 07:06 cxzgb123

@cxzgb123 鸟哥,最后3题能给个分析么,没搞明白是为什么.....

  1. map不是协程安全的
  2. slice不是协程安全的
  3. for循环是拷贝

elvismacak avatar Jul 11 '19 08:07 elvismacak

@smallnest 正确答案加上了,回答的还不错。

@PureWhiteWu

  1. D
  2. D
  3. D
  4. B
  5. D
  6. 不熟悉Pool,不知道……
  7. C
  8. D
  9. A
  10. B

另:求正确答案……

5不正确,是程序阻塞,因为mu2的状态是lock状态

jiashiran avatar Jul 18 '19 08:07 jiashiran

感谢。要是谁能够给每题答案加上注解就更好了

YJinHai avatar Jul 19 '19 03:07 YJinHai

@smallnest 正确答案加上了,回答的还不错。

@PureWhiteWu

  1. D
  2. D
  3. D
  4. B
  5. D
  6. 不熟悉Pool,不知道……
  7. C
  8. D
  9. A
  10. B

另:求正确答案……

5不正确,是程序阻塞,因为mu2的状态是lock状态

死锁会报 panic dead lock 的

LooJee avatar Jul 20 '19 00:07 LooJee

https://www.quora.com/Why-can-a-mutex-be-used-as-a-memory-barrier mutex should hold an memory barrier, case 4 should be OK

xuchao1129 avatar Aug 30 '19 02:08 xuchao1129

想问一下, sync.Pool 那段代码, quiz6, 是因为申请内存的频率和大小与GC的不一样吗?还是有其他的含义. 我改了原代码, 能看到内存占用比较低(有突刺).

基于 go1.13.1

package main

import (
	"bytes"
	"fmt"
	"runtime"
	"sync"
	"time"
)

var pool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}

func main() {
	go func() {
		for {
			time.Sleep(1e9)
			processRequest(1 << 28) // 256MiB
		}
	}()
	for i := 0; i < 1000; i++ {
		go func() {
			for {
				time.Sleep(1e9)
				processRequest(1 << 10) // 1KiB
			}
		}()
	}

	var stats runtime.MemStats
	for i := 0; ; i++ {
		runtime.ReadMemStats(&stats)
		before := stats.Alloc
		fmt.Printf("Cycle %d, Before GC: %dB\n", i, before)
		// time.Sleep(time.Second)
		runtime.GC()
		runtime.ReadMemStats(&stats)
		after := stats.Alloc
		fmt.Printf("Cycle %d, After GC: %dB\n", i, after)
		fmt.Printf("%v\n", before < after)
	}
}

func processRequest(size int) {
	b := pool.Get().(*bytes.Buffer)
	time.Sleep(500 * time.Millisecond)
	b.Grow(size)
	pool.Put(b)
	time.Sleep(1 * time.Millisecond)
}

Lonenso avatar Apr 27 '20 11:04 Lonenso

如果有人对sync.Pool那个例子感到困惑, 看看这个 https://github.com/golang/go/issues/23199

Lonenso avatar May 07 '20 09:05 Lonenso