2024-04-24
学习
00
请注意,本文编写于 415 天前,最后修改于 291 天前,其中某些信息可能已经过时。

目录

1 Mutex
2 RWMutex
3 Waitgroup
4 双检查实现单例
5 Mutex
6 Pool
7 channel
8 channel
9 Map
10 happens before
答案

Go 并发编程测试题----

1 Mutex

go
package main import ( "fmt" "sync" ) var mu sync.Mutex var chain string func main() { chain = "main" A() fmt.Println(chain) } func A() { mu.Lock() defer mu.Unlock() chain = chain + " --> A" B() } func B() { chain = chain + " --> B" C() } func C() { mu.Lock() defer mu.Unlock() chain = chain + " --> C" }

A: 不能编译

B: 输出 main --> A --> B --> C

C: 输出 main

D: panic

2 RWMutex

go
package main import ( "fmt" "sync" "time" ) var mu sync.RWMutex var count int func main() { go A() time.Sleep(2 * time.Second) mu.Lock() defer mu.Unlock() count++ fmt.Println(count) } func A() { mu.RLock() defer mu.RUnlock() B() } func B() { time.Sleep(5 * time.Second) C() } func C() { mu.RLock() defer mu.RUnlock() }

A: 不能编译

B: 输出 1

C: 程序hang住

D: panic

3 Waitgroup

go
package main import ( "sync" "time" ) func main() { var wg sync.WaitGroup wg.Add(1) go func() { time.Sleep(time.Millisecond) wg.Done() wg.Add(1) }() wg.Wait() }

A: 不能编译

B: 无输出,正常退出

C: 程序hang住

D: panic

4 双检查实现单例

go
package doublecheck import ( "sync" ) type Once struct { m sync.Mutex done uint32 } func (o *Once) Do(f func()) { if o.done == 1 { return } o.m.Lock() defer o.m.Unlock() if o.done == 0 { o.done = 1 f() } }

A: 不能编译

B: 可以编译,正确实现了单例

C: 可以编译,有并发问题,f函数可能会被执行多次

D: 可以编译,但是程序运行会panic

5 Mutex

go
package main import ( "fmt" "sync" ) type MyMutex struct { count int sync.Mutex } func main() { var mu MyMutex mu.Lock() var mu2 = mu mu.count++ mu.Unlock() mu2.Lock() mu2.count++ mu2.Unlock() fmt.Println(mu.count, mu2.count) }

A: 不能编译

B: 输出 1, 1

C: 输出 1, 2

D: panic

6 Pool

go
package main import ( "bytes" "fmt" "runtime" "sync" "time" ) var pool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }} func main() { go func() { for { processRequest(1 << 28) // 256MiB } }() for i := 0; i < 1000; i++ { go func() { for { processRequest(1 << 10) // 1KiB } }() } var stats runtime.MemStats for i := 0; ; i++ { runtime.ReadMemStats(&stats) fmt.Printf("Cycle %d: %dB\n", i, stats.Alloc) time.Sleep(time.Second) runtime.GC() } } 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) }

A: 不能编译

B: 可以编译,运行时正常,内存稳定

C: 可以编译,运行时内存可能暴涨

D: 可以编译,运行时内存先暴涨,但是过一会会回收掉

7 channel

go
package main import ( "fmt" "runtime" "time" ) func main() { var ch chan int go func() { ch = make(chan int, 1) ch <- 1 }() go func(ch chan int) { time.Sleep(time.Second) <-ch }(ch) c := time.Tick(1 * time.Second) for range c { fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine()) } }

A: 不能编译

B: 一段时间后总是输出 #goroutines: 1

C: 一段时间后总是输出 #goroutines: 2

D: panic

8 channel

go
package main import "fmt" func main() { var ch chan int var count int go func() { ch <- 1 }() go func() { count++ close(ch) }() <-ch fmt.Println(count) }

A: 不能编译

B: 输出 1

C: 输出 0

D: panic

9 Map

go
package main import ( "fmt" "sync" ) func main() { var m sync.Map m.LoadOrStore("a", 1) m.Delete("a") fmt.Println(m.Len()) }

A: 不能编译

B: 输出 1

C: 输出 0

D: panic

10 happens before

go
package main var c = make(chan int) var a int func f() { a = 1 <-c } func main() { go f() c <- 0 print(a) }

A: 不能编译

B: 输出 1

C: 输出 0

D: panic

答案

  1. D
    会产生死锁panic,因为Mutex 是互斥锁。在 A 函数中,获取了互斥锁 mu,然后调用 B 函数。在 B 函数中又调用了 C 函数。 在 C 函数中,再次尝试获取已经被 A 函数获取且尚未释放的互斥锁 mu,这会导致死锁,从而引发 panic 。 因为一个互斥锁在同一时刻不能被多次获取,除非先释放之前获取的锁。 综上所述,这段代码会 panic 而不是完成正常的执行并输出特定结果,所以选择 D 选项。

  2. D
    首先,在 main 函数中启动了一个 goroutine 执行 A 函数,然后 main 函数睡眠 2 秒后尝试获取写锁(mu.Lock)。 在 A 函数中获取了读锁(mu.RLock),然后调用 B 函数。 在 B 函数中睡眠 5 秒后调用 C 函数。 在 C 函数中又获取了读锁。 由于读锁可以被多个 goroutine 同时持有,所以 A、B、C 中的读锁获取都能成功。 但是,当 main 函数在 2 秒后尝试获取写锁时,由于此时还有读锁未释放(A、B、C 中的读锁),所以写锁会被阻塞等待所有读锁释放。而读锁的操作还在进行并且需要 5 秒以上,所以会导致 main 函数在此处一直等待,从而使程序 hang 住。 综上所述,程序会因为写锁等待读锁释放而 hang 住,所以选择 D 选项。

  3. D
    sync.WaitGroup 的 Wait 方法用于等待所有的任务完成(即 Add 增加的计数通过 Done 减为 0)。 在给定的代码中,启动的 goroutine 中先调用 wg.Done() 减少计数,然后又调用 wg.Add(1) 增加计数。 然而,当 wg.Wait() 被调用时,如果此时之前增加的计数已经通过 Done 减为 0 了,再去调用 Add(1) 就会导致 panic ,因为这违反了 WaitGroup 的使用规则,即在 Wait 已经开始等待之后,不应该再修改计数。 综上所述,这段代码会导致 panic,所以选择 D 选项。

  4. C
    在 Do 方法中,首先进行了一次 done 状态的检查(if o.done == 1)。这是第一次检查,如果 done 已经为 1,表示函数 f 已经执行过,直接返回。 然后获取锁(o.m.Lock()),再次检查 done 的状态(if o.done == 0)。这是第二次检查,确保在获取锁之后,done 仍然为 0 ,才执行函数 f 并将 done 设置为 1 。 这种双重检查的目的是为了减少获取锁的开销。在大多数情况下,如果第一次检查 done 为 1 ,就不需要获取锁,从而提高性能。 然而,这种双重检查锁在没有适当的同步机制下可能会出现问题。在 Go 语言中,由于内存模型的原因,对 done 的读和写操作可能不会被其他 goroutine 按照预期的顺序看到,可能导致不一致的结果。 但在上述代码中,通过使用 sync.Mutex 来保证了对 done 变量的读写操作的原子性和可见性,使得双重检查锁的实现是正确有效的,从而避免了并发访问导致的错误,所以选择 C 选项。

  5. D
    当创建 mu2 并将 mu 赋值给 mu2 时,这是值拷贝,而不是引用拷贝。 mu2 拥有自己独立的 Mutex 锁和 count 变量。 当对 mu 进行加锁、修改 count 、解锁的操作后,再对 mu2 进行加锁、修改 count 、解锁的操作,这两组操作是完全独立的,彼此之间没有影响。 所以最终 mu.count 和 mu2.count 都是各自独立增加了 1 ,结果都为 1 。 综上所述,最终打印的结果是 1 1 ,选择 D 选项。

  6. C
    在这段代码中: 首先,代码是可以编译成功的,所以 A 选项不正确。 程序中启动了一个 Goroutine 不断处理 256 MiB 的内存请求,同时启动了 1000 个 Goroutine 不断处理 1 KiB 的内存请求。 虽然使用了 sync.Pool 来复用 bytes.Buffer 对象,但是对于 256 MiB 这样的大内存请求,sync.Pool 可能无法及时有效地回收和复用这些内存。 由于不断有新的 256 MiB 的内存分配需求,而且这些大内存块可能不会很快被回收和复用,导致内存的使用可能会快速增长,出现内存暴涨的情况。 虽然周期性地触发了垃圾回收,但也不能保证能够及时有效地回收这些大内存块,从而使得内存可能会持续增长,而不一定会像 D 选项描述的那样过一会就回收掉。 综上所述,运行时内存可能会暴涨,选择 C 选项。

  7. C
    首先,代码可以编译成功,所以 A 选项不正确。 在 main 函数中,启动了两个协程: 第一个协程中创建了一个有缓冲的通道 ch 并向其中发送了一个值。 第二个协程在等待 1 秒后从通道 ch 接收一个值。 主程序中有一个 time.Tick 每 1 秒触发一次的循环,用于打印当前的协程数量。 由于第二个协程在等待 1 秒后才接收通道的值,而在这 1 秒内,第一个协程已经在运行并且还未完成(因为还没有被接收),同时主程序的循环也在运行,所以一段时间后总是会有两个协程在运行,输出 #goroutines: 2 。 B 选项不正确,因为不会总是只有 1 个协程在运行。 D 选项不正确,代码中没有会导致 panic 的情况。 综上所述,选择 C 选项。

  8. D
    在这个程序中,首先定义了一个未初始化的 chan int 类型的通道 ch 。 然后启动了两个协程,第一个协程尝试向未初始化的通道 ch 发送值,这会导致运行时错误,引发 panic 。 在向一个未初始化(即 nil )的通道发送或接收值时,Go 语言会抛出运行时恐慌。 A 选项,代码可以编译,但运行时会出问题。 B 选项和 C 选项,由于在向通道发送值时就已经发生了 panic ,所以无法执行到后续打印 count 的部分。 综上所述,选择 D 选项。

  9. A
    sync.Map 的 Len 方法并不存在。所以这段代码无法通过编译。 sync.Map 没有提供直接获取元素数量的方法,因此无法直接获取其长度或元素个数。 B 选项,因为无法获取长度,所以不会输出 1 。 C 选项,同理,也不会输出 0 。 D 选项,代码由于语法错误无法编译,而不是在运行时发生 panic 。 综上所述,选择 A 。

  10. B
    在 main 函数中,启动了一个 go 协程 f,在 f 函数中,先将变量 a 赋值为 1,然后等待从通道 c 接收数据。 在 main 函数中,向通道 c 发送数据 0。 当发送数据到通道 c 后,f 函数中的 <-c 操作得以继续,此时 a 已经被赋值为 1 。 最后在 main 函数中打印 a 的值,所以输出 1 。 A 选项,代码可以编译成功。 C 选项,因为在 f 函数中 a 已经被修改为 1 ,所以不会输出 0 。 D 选项,代码运行过程中没有导致 panic 的情况。 综上所述,选择 B 。

本文作者:han

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!