MST
星途 面试题库

面试题:Go中协程与线程的区别

请详细阐述Go语言中协程(goroutine)和传统线程的区别,包括资源占用、调度方式、并发模型等方面。
43.6万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

资源占用

  1. 传统线程
    • 线程的栈空间通常较大,一般为几MB。这是因为传统线程需要为函数调用栈等预留足够空间,较大的栈空间虽然能满足复杂函数调用链的需求,但在大量线程创建时会消耗大量内存。
    • 每个线程的创建和销毁开销较大,包括操作系统内核为其分配和回收资源(如内存空间、内核数据结构等)的操作,这使得创建和销毁大量线程时系统性能会受到较大影响。
  2. Go协程(goroutine)
    • goroutine的栈空间初始非常小,一般只有2KB左右。其栈空间是动态增长和收缩的,在需要时会自动扩展,这大大降低了内存占用。例如,一个简单的只执行基本计算的goroutine可能一直保持初始的小栈空间,而一个有深层递归调用的goroutine会在递归过程中按需扩展栈空间。
    • goroutine的创建和销毁开销极小。创建goroutine主要是在用户态进行一些简单的栈初始化和调度相关数据结构的设置,无需进入内核态进行复杂的资源分配,销毁时同样也只是进行简单的清理操作,这使得可以轻松创建数以万计的goroutine。

调度方式

  1. 传统线程
    • 由操作系统内核进行调度,采用的是抢占式调度算法。操作系统内核通过时间片轮转等方式来分配CPU时间给各个线程,当一个线程的时间片用完,内核会强制将其挂起并调度其他线程运行。这种调度方式在多核心CPU环境下能有效利用硬件资源,但由于涉及内核态与用户态的切换,开销较大。例如,当一个线程进行系统调用时,会从用户态切换到内核态,完成系统调用后再切换回用户态,这个过程涉及大量的上下文切换操作,对性能有一定影响。
  2. Go协程(goroutine)
    • 由Go语言运行时(runtime)的调度器进行调度,采用协作式调度(也叫非抢占式调度)。goroutine在遇到I/O操作、系统调用、显式调用runtime.Gosched()等情况时,会主动让出CPU,让调度器有机会调度其他goroutine运行。这种调度方式避免了频繁的内核态与用户态切换,调度开销小。例如,一个执行网络I/O操作的goroutine在发起I/O请求后,会主动暂停,让调度器可以调度其他可运行的goroutine,而无需等待I/O完成。

并发模型

  1. 传统线程
    • 常使用共享内存模型来实现并发编程。多个线程可以访问和修改共享的内存数据,为了保证数据的一致性和避免竞态条件,需要使用锁(如互斥锁、读写锁等)、信号量等同步机制。然而,共享内存模型容易出现死锁、数据竞争等问题,编程难度较大。例如,两个线程同时对共享变量进行写操作,如果没有正确使用锁,就可能导致数据不一致。
  2. Go协程(goroutine)
    • 倡导使用“通信顺序进程”(CSP)模型,通过通道(channel)进行数据通信来实现并发间的同步和数据共享。goroutine之间通过通道发送和接收数据,这种方式从设计上减少了共享内存带来的问题,使得并发编程更加安全和简洁。例如,一个goroutine可以将数据发送到通道,另一个goroutine从通道接收数据,从而实现数据的传递和协同工作,避免了直接共享内存带来的复杂性。