浅谈:Goroutine阻塞、状态以及并发模型及注意事项

1,625 阅读4分钟

在面试过程中经常被问到关于Goroutine的问题。在这篇博客中,我将结合面试官的问题,详细讨论Goroutine的阻塞、状态以及内存管理问题,并分享一些实际项目中的经验和注意事项。

线程模型和Goroutine

1. Goroutine何时会发生阻塞?

Goroutine是Go语言中的轻量级线程,它们在Go运行时(runtime)中被调度和管理。Goroutine在以下几种情况下可能会发生阻塞:

  • 阻塞在系统调用:当Goroutine执行一个阻塞的系统调用(如文件I/O、网络I/O等)时,它会被阻塞,直到系统调用完成。
  • 阻塞在通道操作:Goroutine在执行通道(channel)操作时,如发送数据到一个已满的通道或从一个空的通道接收数据,可能会发生阻塞。阻塞会持续到通道中有足够的空间或数据可用。
  • 阻塞在同步原语:Goroutine在等待锁(如sync.Mutex)或其他同步原语(如sync.WaitGroup)时,可能会发生阻塞。
  • 阻塞在垃圾回收:在垃圾回收过程中,所有Goroutine都可能会短暂地被阻塞,以便垃圾回收器完成内存回收。

2. PMG模型中Goroutine的状态

在Go的调度模型(PMG模型)中,Goroutine有以下几种状态:

  • Gidle:空闲状态,表示Goroutine未被调度执行。
  • Grunnable:可运行状态,表示Goroutine已经准备好运行,等待被调度到一个线程(M)上执行。
  • Grunning:运行状态,表示Goroutine正在一个线程(M)上执行。
  • Gsyscall:系统调用状态,表示Goroutine正在执行阻塞的系统调用。
  • Gwaiting:等待状态,表示Goroutine正在等待某个事件(如通道操作、同步原语等)。
  • Gdead:死亡状态,表示Goroutine已经执行完成或被终止。

3. Goroutine占用资源过多的问题及PMG模型的解决方案

当一个Goroutine占用过多的资源(如内存、CPU时间等),可能会导致其他Goroutine的性能受到影响。为了解决这个问题,PMG模型采用了以下策略:

  • 工作窃取(Work Stealing):当一个线程(M)上的Goroutine执行完毕,而其他线程仍有Goroutine等待执行时,空闲的线程会尝试“窃取”其他线程的Goroutine来执行。这样可以平衡各个线程的负载,避免某个线程上的Goroutine长时间占用资源。
  • 抢占式调度(Preemptive Scheduling):Go运行时会定期检查Goroutine的执行状态,如果发现某个Goroutine长时间占用CPU,运行时会尝试抢占该Goroutine,让其他Goroutine有机会执行。这样可以避免某个Goroutine“霸占”CPU资源,影响其他Goroutine的执行。

4. 线程和Goroutine的OOM问题

当一个线程发生OOM(内存溢出)时,通常会导致整个进程崩溃,因为线程共享进程的地址空间。然而,当一个Goroutine发生OOM时,情况可能会有所不同。由于Goroutine的堆栈是动态扩展的,当一个Goroutine的堆栈无法扩展时,Go运行时会尝试回收其他Goroutine的内存,以便为当前Goroutine分配更多的内存。如果回收失败,Go运行时会抛出一个运行时错误(如runtime: out of memory),但不会导致整个进程崩溃。

5. 项目中的OOM问题及解决方法

在实际项目中,我曾遇到过OOM问题。为了解决这个问题,我采取了以下措施:

  1. 优化内存分配:避免频繁分配和释放内存,尽量复用已分配的内存。
  2. 使用内存池:为频繁使用的对象创建内存池,以减少内存分配和垃圾回收的开销。
  3. 限制Goroutine的数量:使用协程池来限制并发的Goroutine数量,避免过多的Goroutine导致内存耗尽。
  4. 监控内存使用:使用工具(如pprof)定期监控程序的内存使用情况,及时发现和解决内存泄漏问题。

6. Goroutine中的panic问题

当一个Goroutine发生panic时,它会导致当前Goroutine的执行终止,并在调用栈上执行延迟函数(defer)。如果没有捕获到panic(使用recover函数),则会导致整个进程崩溃。需要注意的是,一个Goroutine的panic不会影响其他Goroutine的执行。

关于defer和panic,有一个重要的注意事项:defer函数只能捕获到当前Goroutine的panic,而不能捕获子Goroutine的panic。因此,在使用Goroutine时,需要确保在每个Goroutine内部处理好panic,避免程序崩溃。