写点什么

过年前 再带大家卷一波 Go 高质量知识点

作者:王中阳Go
  • 2024-02-06
    北京
  • 本文字数:4655 字

    阅读完需:约 15 分钟

过年前 再带大家卷一波Go高质量知识点

以下内容来自 Go就业训练营 的提问和答疑。

1. ⼀个 T 类型的值可以调⽤*T 类型声明的⽅法,当且仅当 T 是可寻址的。(怎么理解可寻址)

可寻址是指能够获取变量的内存地址。

在 Go 语言中,以下类型的值是可寻址的:
  1. 值类型(Value Types):包括基本类型(如整数、浮点数、布尔值等)和结构体(struct)类型。可以通过取地址操作符 & 来获取变量的内存地址。

  2. 数组(Array):数组的元素是值类型,因此数组的元素也是可寻址的。

  3. 切片(Slice):切片是对数组的引用,通过索引操作可以获取切片中的元素的地址。

  4. 指针(Pointer):指针本身就是存储变量内存地址的类型,因此指针是可寻址的。

以下类型的值是不可寻址的:
  1. 常量(Constants):常量是不可变的,因此没有内存地址。

  2. 字符串(String):字符串是不可变的,因此没有内存地址。

  3. 字面量(Literals):字面量是直接使用的常量值,没有对应的变量,因此没有内存地址。


理解可寻址的概念对于理解 Go 语言中方法的调用和接收者的限制非常重要:只有当一个类型是可寻址的,才能够调用该类型的指针接收者方法。这是因为指针接收者方法需要在方法内部修改接收者的状态,而只有可寻址的值才能被修改。

2. 三⾊标记法若不被 STW 保护可能会导致对象丢失,⽩⾊对象被⿊⾊对象引⽤,灰⾊对象对⽩⾊对象的引⽤丢失(为什么需要这个条件),导致对象丢失。

  1. 三色标记法是一种用于垃圾回收的算法,用于标记和回收不再使用的对象。在三色标记法中,对象被标记为三种不同的颜色:白色、灰色和黑色。

  2. 在垃圾回收过程中,白色对象表示未被访问的对象,灰色对象表示正在被访问的对象,黑色对象表示已经被访问并且是可达的对象。

  3. 在三色标记法中,灰色对象对白色对象的引用是非常重要的。这是因为灰色对象表示正在被访问的对象,如果灰色对象对白色对象的引用丢失,那么这个白色对象将无法被访问到,也就无法被正确地标记为可达的对象。如果灰色对象对白色对象的引用丢失,那么在垃圾回收过程中,这个白色对象将被错误地标记为不可达的对象,从而导致对象丢失。这可能会导致内存泄漏或错误地回收仍然可达的对象。

  4. 建议你再好好看下这个教程:Golang三色标记混合写屏障GC模式全分析

  5. 有哪些不理解的可以看 b 站对应的视频教程,第 9 章之后只看文档理解起来可能还是有些吃力的:Golang GC详解视频

3. 逃逸分析相关的问题,堆有时需要加锁:堆上的内存,有时需要加锁防⽌多线程冲突,为什么堆上的内存有时需要加锁?⽽不是⼀直需要加锁呢?

  1. 堆上的内存有时需要加锁是因为堆上的内存是被多个线程共享的,当多个线程同时访问和修改堆上的内存时,可能会发生并发冲突。 为了保证数据的一致性和避免竞态条件,需要对堆上的内存进行加锁。

  2. 然而,并不是所有情况下都需要对堆上的内存进行加锁。加锁会引入额外的开销,并且可能导致性能下降。

  3. 我们可以通过其他方式来避免对堆上的内存进行加锁。例如,**可以使用无锁数据结构、使用分段锁或使用事务等技术来减少对堆上内存的竞争,提高并发性能。**这些方法可以通过设计合理的数据结构和算法来避免并发冲突,从而不需要对堆上的内存进行加锁。

  4. 最后总结一下:是否需要对堆上的内存进行加锁取决于具体的并发场景和需求。在一些情况下,可以通过其他方式来避免对堆上内存的竞争,提高并发性能。只有在确实存在并发冲突的情况下,才需要对堆上的内存进行加锁来保证数据的一致性和避免竞态条件。


为了让你更好的理解,我再补充一下:

下面这些情况就不需要对堆上的内存加锁:
  1. 只读操作: 如果多个线程只是对堆上的内存进行读取操作,并且没有写操作,那么不需要对堆上的内存进行加锁。只读操作不会引起并发冲突,因此不需要额外的同步措施。

  2. 无共享状态: 如果多个线程之间没有共享状态,即它们访问的是独立的堆上内存,那么也不需要对堆上的内存进行加锁。每个线程操作的是自己独立的内存,不存在并发冲突的问题。

  3. 无竞争条件: 如果多个线程对堆上的内存进行操作,但它们之间没有竞争条件,即它们的操作不会相互干扰或产生不一致的结果,那么也不需要对堆上的内存进行加锁。

  4. 使用无锁数据结构: 如果使用了无锁数据结构,例如原子操作、无锁队列等,这些数据结构本身已经提供了并发安全的操作,不需要额外的加锁。

  5. 以上这些提供了思考的角度,具体是否需要对堆上的内存进行加锁取决于具体的并发场景和需求。

4. 堆内存具体是如何分配的,由谁持有?mcache :线程缓存,mcentral :中央缓存,mheap :堆内存,线程缓存 mcache?

在 Go 语言中,堆内存的分配是由 Go 运行时(runtime)负责管理的。


下面是堆内存分配的一般过程:


  1. 线程缓存(mcache):每个逻辑处理器(P)都有一个线程缓存(mcache),用于存储一些预分配的内存块。线程缓存是每个线程独立拥有的,用于提高内存分配的性能。

  2. 中央缓存(mcentral):中央缓存是全局共享的,用于存储更多的内存块。当线程缓存不足时,会从中央缓存获取更多的内存块。

  3. 堆内存(mheap):如果中央缓存也没有足够的内存块,Go 运行时会从堆内存中获取更多的内存。堆内存是用于存储动态分配的对象的区域。


在堆内存的分配过程中,线程缓存(mcache)被逻辑处理器(P)持有。每个逻辑处理器都有自己的线程缓存,用于存储预分配的内存块。当线程缓存不足时,逻辑处理器会从中央缓存(mcentral)获取更多的内存块。如果中央缓存也不足,逻辑处理器会从堆内存(mheap)中获取更多的内存。


通过这种机制,Go 运行时可以高效地管理和分配堆内存,同时减少对全局锁的竞争。每个逻辑处理器都有自己的线程缓存,从而减少了对共享资源的竞争,提高了并发性能。

5. 编译器通过逃逸分析去选择内存分配到堆或者栈? ⽣命周期不可知的情况有哪些?是发生指针逃逸吗?

  1. 首先第一个提问的理解是正确的。

生命周期不可知的情况包括:
  1. 指针逃逸:当一个指针被返回给函数的调用者、存储在全局变量中或逃逸到堆上时,编译器无法确定指针的生命周期。

  2. 闭包:当一个函数内部定义的闭包引用了外部的变量,并且这个闭包被返回、存储在全局变量中或逃逸到堆上时,编译器无法确定闭包的生命周期。

  3. 并发编程:在并发编程中,如果一个变量被多个 Goroutine 共享访问,并且可能在 Goroutine 之间传递或逃逸到堆上时,编译器无法确定变量的生命周期。

6. 如果生命周期可知,则一定在栈上分配吗? 如果不可知,则认为内存逃逸,必须在堆上分配?

  1. 如果变量的生命周期是完全可知的,编译器会优先将其分配在栈上。 这是因为栈上的内存分配和释放是非常高效的,仅仅需要移动栈指针即可完成。栈上的内存分配是自动管理的,当变量超出作用域时,栈上的内存会自动释放。

  2. 注意:并不是所有生命周期可知的变量都一定在栈上分配。 编译器可能会根据一些其他的因素来决定内存的分配位置。例如,如果变量的大小较大,超过了栈的限制,编译器可能会选择在堆上分配内存。

  3. 后面的提问你的理解是正确的:如果变量的生命周期不可知,编译器会认为它会逃逸到堆上,并在堆上进行分配。逃逸到堆上的变量可以在函数返回后继续被访问,或者被其他函数或 Goroutine 引用。在这种情况下,编译器无法确定变量的生命周期,因此选择在堆上分配内存。

  4. 注意:编译器的具体实现可能会有所不同,不同的编译器可能会有不同的策略来处理内存的分配。因此,虽然生命周期可知的变量通常会在栈上分配,但并不是绝对的规则。编译器会根据具体情况进行优化和决策,以提高程序的性能和效率。

7. mutex 和原⼦锁混⽤导致 mutex 失效的情况和原因?

  1. 重复加锁和解锁:如果在使用 Mutex 和原子锁时混淆了它们的使用,可能会导致重复加锁和解锁的问题。例如,使用 Mutex 加锁后又使用原子锁进行操作,然后再次使用 Mutex 解锁。这种混乱的加锁和解锁顺序可能导致互斥锁的状态不一致,从而使 Mutex 失去了正确的同步效果。

  2. 未正确保护共享资源:Mutex 和原子锁的目的是保护共享资源的访问,但如果在使用它们时没有正确地保护共享资源,也会导致 Mutex 失效。例如,使用 Mutex 加锁后,但在访问共享资源时使用了原子操作而没有使用 Mutex 进行保护,这样其他线程可能会在没有正确同步的情况下访问共享资源。

  3. 不一致的同步策略:Mutex 和原子锁是不同的同步机制,它们有不同的语义和使用方式。如果在同一个代码块或函数中混用了 Mutex 和原子锁,可能会导致不一致的同步策略。例如,一个线程使用 Mutex 加锁后,另一个线程使用原子锁进行操作,这样就无法保证正确的同步和互斥访问。

  4. 竞态条件和数据竞争:混用 Mutex 和原子锁时,如果没有正确地处理竞态条件和数据竞争,也会导致 Mutex 失效。竞态条件是指多个线程对共享资源的访问顺序不确定,可能导致不一致的结果。数据竞争是指多个线程同时访问和修改共享资源,可能导致数据的不确定性和不一致性。

  5. 总结一下也就是:在并发编程中,需要遵循一致的同步策略,正确地使用 Mutex 或原子锁来保护共享资源的访问。混用 Mutex 和原子锁时,需要确保加锁和解锁的顺序正确,并保证所有对共享资源的访问都经过正确的同步机制。

8. Go 语言互斥锁的问题,解锁后会发出信号量通知阻塞的协程:若有多个协程阻塞,如何保证只有⼀个协程被唤

醒?若存在饥饿模式如何保证处于饥饿模式的协程优先获得锁?


  1. 在互斥锁的实现中,解锁后会发出信号量通知阻塞的协程,但是并不能保证只有一个协程被唤醒。多个协程可能同时被唤醒,然后竞争互斥锁。这是因为互斥锁的唤醒是由操作系统的调度器来控制的,调度器可能会同时唤醒多个协程。

  2. 为了确保只有一个协程被唤醒,可以结合条件变量(Cond)来实现。条件变量可以与互斥锁一起使用,通过条件变量的 Wait() 和 Signal() 或 Broadcast() 方法来实现协程的唤醒和等待。当互斥锁解锁时,可以使用条件变量的 Signal() 方法来唤醒一个协程,或使用 Broadcast() 方法唤醒所有协程。这样可以确保只有一个或一组协程被唤醒,其他协程仍然保持阻塞状态。

  3. 对于饥饿模式,可以使用公平锁(Fair Mutex)来解决。我在下面给你写了一个代码示例:我们自定义了一个 FairMutex 结构体,其中使用了 sync.Cond 来实现条件变量。公平锁会按照请求的顺序来分配锁,确保等待时间最长的协程优先获得锁,从而避免饥饿问题。

  4. 在 FairMutex 的 Lock() 方法中,使用了 for 循环来等待锁的释放,确保只有一个协程被唤醒。当锁被解锁时,使用条件变量的 Signal() 方法来唤醒一个协程,而其他协程仍然保持阻塞状态。

示例代码
package main    import (  "fmt"  "sync"  )    type FairMutex struct {  mu sync.Mutex  cond *sync.Cond  isLocked bool  }    func NewFairMutex() *FairMutex {  return &FairMutex{  cond: sync.NewCond(&sync.Mutex{}),  isLocked: false,  }  }    func (fm *FairMutex) Lock() {  fm.mu.Lock()  defer fm.mu.Unlock()    for fm.isLocked {  fm.cond.Wait()  }  fm.isLocked = true  }    func (fm *FairMutex) Unlock() {  fm.mu.Lock()  defer fm.mu.Unlock()    fm.isLocked = false  fm.cond.Signal()  }    func main() {  fm := NewFairMutex()    var wg sync.WaitGroup  for i := 0; i < 5; i++ {  wg.Add(1)  go func(id int) {  fm.Lock()  defer fm.Unlock()    fmt.Printf("Goroutine %d acquired the lock\n", id)  // Do some work...  fmt.Printf("Goroutine %d released the lock\n", id)    wg.Done()  }(i)  }    wg.Wait()  }  
复制代码
运行结果

欢迎关注我

本文首发公众号:王中阳Go


加我微信邀你进 就业跳槽交流群 :wangzhongyang1993

新年快乐

最后祝大家新年快乐,龙年大吉,放假了好好休息,咱们明年继续战斗!



发布于: 刚刚阅读数: 4
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论 (1 条评论)

发布
用户头像
过年前,再卷一波。
刚刚 · 北京
回复
没有更多了
过年前 再带大家卷一波Go高质量知识点_Go_王中阳Go_InfoQ写作社区