GoLang是怎么做到并发的

Go 语言通过内置的 GoroutineChannel 实现高效并发。这是 Go 的核心特性之一,下面我从原理和机制两个角度帮你详细解释:


💡 一、Goroutine:轻量级线程

✅ 什么是 Goroutine?

Goroutine 是 Go 语言中的 轻量级线程,由 Go 运行时(runtime)管理,而不是操作系统。

你只需要使用 go 关键字调用一个函数,就可以创建一个新的 Goroutine:

go myFunction()

✅ 它有多轻量?

✅ 与线程的区别:

特性 Goroutine 操作系统线程
管理者 Go runtime 操作系统内核
栈大小 初始仅约 2KB 通常为 1MB 以上
调度方式 协作式调度(M:N) 抢占式调度(1:1)
创建成本 很低 较高
切换开销 极低 相对较高

🔁 二、Go 调度器:GPM 模型

Go 的并发运行依赖调度器,它采用了 GPM 模型(Goroutine, Processor, Machine):

调度过程简要:

  1. Goroutine 被创建并进入 G 队列。
  2. 调度器从 G 队列中取出一个 G,并分配给一个 P。
  3. P 会找一个空闲的 M(系统线程)来运行这个 G。
  4. 如果当前 G 阻塞(例如网络 IO),该 M 会被重新用于其他 G。

这种机制确保了 高效协作调度,最大化 CPU 使用率。


🔗 三、Channel:通信机制

Go 提供 Channel 作为 Goroutine 之间通信的工具,实现 CSP(通信顺序进程)并发模型。

ch := make(chan int)

go func() {
    ch <- 10 // 发送数据
}()

data := <-ch // 接收数据
fmt.Println(data)

✅ 特性:


🧠 四、Go 并发优势总结

特性 描述
goroutine 简洁、轻量级、低开销
channel 安全的通信机制
runtime调度器(GPM) 高效调度与资源利用
不依赖线程池 天然支持成千上万个任务

✅ 示例:同时爬多个网页

func fetch(url string, ch chan string) {
    // 模拟爬取
    time.Sleep(1 * time.Second)
    ch <- fmt.Sprintf("Fetched %s", url)
}

func main() {
    urls := []string{"a.com", "b.com", "c.com"}
    ch := make(chan string)
    for _, url := range urls {
        go fetch(url, ch)
    }
    for range urls {
        fmt.Println(<-ch)
    }
}

在 Go 语言中,“进程、线程、协程”的概念和调度关系有一套独特的模型,核心是 Go Runtime 的调度器,也就是我们常说的 GPM 模型(Goroutine-Processor-Machine)


🧱 一、GPM基本定义

概念 Go中的体现 说明
进程 OS级进程 运行一个 Go 程序时,会启动一个独立进程,和其他语言一样
线程(Thread) M(Machine) 操作系统线程,由 Go runtime 使用
协程(Coroutine) G(Goroutine) Go 的用户级线程,轻量,调度由 Go runtime 管理

🔁 二、GPM 模型详解(Goroutine - Processor - Machine)

Go runtime 使用 GPM 三个组件来管理并发:

组件 描述
G(Goroutine) 用户级协程,执行单元(函数)
P(Processor) 虚拟处理器,维护可运行 G 的队列,数量 = GOMAXPROCS
M(Machine) 内核线程,由操作系统分配,负责真正执行 G

📌 它们的关系图(概念图):

     Goroutines (G) ---+
                       |
                       v
     Processor (P) ----> Goroutine队列
                       |
                       v
     Machine (M) ------> 调度执行 G

🔄 三、调度流程说明(简化流程)

  1. 启动阶段

  2. 执行阶段

  3. 阻塞处理

  4. 偷取机制(Work Stealing)


💡 四、总结对比

项目 Go Runtime 模型(GPM) 传统操作系统线程模型
协程调度 Go 自己实现的 M:N 调度 OS 1:1 或 N:1 调度
协程数量 数万以上 一般线程几百就瓶颈
切换开销 非常低(用户态完成) 较高(内核态切换)
阻塞行为 自动让出线程 整个线程阻塞

📚 五、例子说明调度

func task(id int) {
    fmt.Printf("Goroutine %d start\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Goroutine %d end\n", id)
}

func main() {
    for i := 0; i < 10; i++ {
        go task(i)
    }
    time.Sleep(2 * time.Second)
}

🔍 Bonus:GOMAXPROCS 的意义

你可以通过 runtime.GOMAXPROCS(N) 设置并发使用的 CPU 核数,也就是最多有几个 P 在调度 G。

runtime.GOMAXPROCS(runtime.NumCPU())

怎么做到非阻塞的,比如监听套接字,读写文件,read write

Go 语言之所以能够实现 非阻塞 的并发编程,关键在于 Go runtime 自己实现了一套调度器和 IO 多路复用机制,屏蔽了操作系统底层阻塞的复杂性。以下是它背后的原理 👇


🧠 一、Go 的“非阻塞”不是你想的“非阻塞”

Go 中调用 net.Conn.Read()os.File.Read() 这些接口在代码层面看是阻塞的,但底层实现为你做了“魔法”——即:

表面上是同步阻塞,实际上是异步非阻塞 + 协程挂起 + 调度恢复

你不需要写回调,也不用关心 epoll/kqueue,一切由 Go runtime 完成 ✅


🔍 二、底层原理:Go Runtime + epoll/kqueue + 协程切换

1️⃣ 网络 IO:基于 epoll(Linux)/ kqueue(BSD/macOS)

Go 的网络库使用 非阻塞套接字 + epoll/kqueue/IOCP + runtime poller

流程如下:

  1. Read() 时,如果数据不可读:

  2. 整个过程中:

👉 底层通过 runtime/netpoll 模块实现 IO 多路复用机制。


2️⃣ 文件 IO:平台相关(非完美)

文件 IO 在不同平台行为不一样:

平台 是否异步支持 实现方式
Linux ❌ 默认是阻塞 Go 采用线程池辅助调度
Windows ✅ IOCP 支持 异步操作系统支持
macOS 用额外线程做阻塞 IO

✅ Linux下的策略(最常见):


🧩 三、read/write 系统调用是怎么处理的?

假如你调用:

conn.Read(buf)

实际上 Go 会:

  1. 将连接设置为非阻塞(fcntl(fd, F_SETFL, O_NONBLOCK)

  2. 尝试调用 read()

Go 的这一机制叫做 网络调度器(netpoller)


🧪 四、关键模块:Go源码模块介绍

模块 作用说明
runtime/netpoll.go epoll/kqueue 封装
internal/poll/fd.go 文件描述符读写封装
runtime/proc.go GPM调度器核心
runtime/os_linux.go OS平台适配

你可以看到 runtime/internal/netpoll 模块中有 epoll 的实现逻辑。


✅ 五、总结:Go 如何实现非阻塞 IO?

特性 实现方式
网络 socket 非阻塞 + epoll + Goroutine 挂起/恢复
文件 IO(Linux) 阻塞 IO + 线程池(worker goroutine)
系统调用阻塞 Goroutine 会挂起,不影响其他协程执行
程序员视角 看起来是“同步阻塞”,实际上是异步调度

📌 举个例子:socket 读写协程挂起

conn, _ := net.Dial("tcp", "example.com:80")

go func() {
    buf := make([]byte, 1024)
    n, err := conn.Read(buf) // 如果不可读,会挂起当前 goroutine
    if err == nil {
        fmt.Println("收到数据:", string(buf[:n]))
    }
}()

这个 Read() 会在数据还没到达时自动让出 CPU,挂起当前 goroutine,等待数据可读再恢复,整个过程无需你自己写 epoll


GO在Linux系统中使用了epoll

Go 在 Linux 系统上底层确实使用了 epoll 来实现网络 IO 的非阻塞调度

是的,✅ Go 在 Linux 系统上底层确实使用了 epoll 来实现网络 IO 的非阻塞调度

这是 Go 实现高效网络并发(尤其是成千上万个 Goroutine 同时进行网络通信)的核心机制之一。


🔍 一、Go 使用 epoll 的位置

在 Linux 下,Go 的网络 IO 使用的是:

这些都是在 Go 的 runtime/netpoll 模块中实现的(Go 源码层面)。


📁 二、源码证据

Go 的源码在以下几个文件中直接或间接使用了 epoll:

示例:epoll 初始化(Go runtime)

func netpollinit() {
    epfd = epollcreate1(_EPOLL_CLOEXEC)
    ...
}
func netpoll(waitfull bool) {
    var events [128]epollevent
    n := epollwait(epfd, &events[0], int32(len(events)), timeout)
    ...
}

这说明:


🧠 三、配合非阻塞 FD 使用

Go 还会把套接字设置为 非阻塞模式

syscall.SetNonblock(fd, true)

然后配合 epoll 的 EPOLLIN, EPOLLOUT 等事件来判断何时能继续 read/write。


📦 四、哪些 Go 包用到了它?

你常用的这些包其实都是底层套了一层 epoll:

包名 是否用到 epoll
net ✅ 用于 TCP/UDP socket
net/http ✅ 用于底层 socket
os 文件 IO ❌ 普通文件用线程池处理(文件 IO 无法用 epoll)

📌 五、为什么自己用 netpoll 的人不多?

因为:


✅ 总结

问题 答案
Go 使用 epoll 吗? ✅ 是的,在 Linux 上用于网络 IO
哪一层使用? runtime 内部的 netpoller(透明封装)
程序员需要关心吗? ❌ 不需要,除非你在写 runtime 或 Cgo 网络库
文件 IO 用 epoll 吗? ❌ 不行,普通文件不支持 epoll,Go 用线程池

如果你想深入查看源码里具体的 epoll 创建、事件监听、Goroutine 唤醒过程,我也可以带你按行分析 runtime/netpoll_epoll.go。是否继续?🧐