Go语言核心机制揭秘:深入浅出GPM模型‌

Go语言核心机制揭秘:深入浅出GPM模型

在 Go 语言的并发编程中,GPM 模型(Goroutine、Processor、Machine)是其实现高效并发的核心机制。本文将从 GPM 模型的概念入手,结合丰富的 代码示例Mermaid 图解,深入浅出地阐释 Go 运行时如何调度 Goroutine、如何利用 OS 线程以及工作窃取等策略,从而帮助你更容易地学习和理解 Go 并发的底层原理。


目录

  1. GPM 模型概述
  2. Goroutine(G)详解
    2.1. Goroutine 的创建与栈管理
    2.2. Goroutine 调度与状态机
  3. Processor(P)详解
    3.1. P 的角色与数量控制(GOMAXPROCS)
    3.2. 本地队列与全局队列
  4. Machine(M)详解
    4.1. M 对应操作系统线程
    4.2. 系统调用与 M 的阻塞/唤醒
  5. GPM 调度器协作流程
    5.1. 工作窃取(Work Stealing)
    5.2. 调度器循环与抢占
    5.3. 阻塞与唤醒示例
  6. 代码示例:并发调度演示
    6.1. 简单高并发 Goroutine 示例
    6.2. 利用 GOMAXPROCS 调整并行度
    6.3. 结合 runtime 包探查 GPM 状态
  7. Mermaid 图解:GPM 调度流程
  8. 调优与常见问题
  9. 小结

1. GPM 模型概述

Go 运行时使用 GPM 模型 来管理并发,其中包含三个核心概念:

  1. G (Goroutine):由 Go 运行时管理的逻辑协程,具有独立的栈(动态增长)与调度状态。
  2. P (Processor):负责将 Goroutine 调度到 OS 线程上执行的“逻辑处理器”,相当于 Goroutine 与 Machine 之间的桥梁。
  3. M (Machine):操作系统线程,最终负责在 CPU 上执行代码。

1.1 为什么需要 GPM?

  • 传统线程(OS Thread)成本高,创建、切换开销大,不适合数百万级并发。
  • Go 用 M\:N 调度,即数以万计的 Goroutine(G)复用到少量 OS 线程(M)上执行。
  • 为了保证并发的高效与可控,引入了“Processor(P)”来管理 Goroutine 的执行上下文,从而实现更细粒度的调度。

Mermaid 简要示意 GPM 关系

flowchart LR
    subgraph Goroutines (G)
        G1[G1] & G2[G2] & G3[G3] & G4[G4]
    end

    subgraph Processors (P)
        P1[P1] & P2[P2]
    end

    subgraph Machines (M)
        M1[M1(Thread)] & M2[M2(Thread)]
    end

    G1 & G2 & G3 & G4 -->|调度| P1 & P2
    P1 --> M1
    P2 --> M2
  • 多个 Goroutine(G1、G2、G3、G4)等待在 P1、P2 上被调度;
  • 每个 P 绑定到一个 M(操作系统线程),M 在 CPU 上执行 G 的用户代码。

2. Goroutine(G)详解

2.1 Goroutine 的创建与栈管理

Goroutine 是 Go 语言最小的并发单元。与传统线程相比,Go 的 Goroutine 具有以下特点:

  • 轻量级:创建代价远小于 OS 线程,初始栈仅 2KB,且可动态扩展至 MB 级。
  • 调度透明:程序员只需使用 go f() 启动 Goroutine,而无需关心 OS 线程如何分配。
  • 独立栈:每个 Goroutine 拥有自己的栈空间,运行时会根据需要自动增长/收缩。

2.1.1 创建 Goroutine

package main

import (
    "fmt"
    "time"
)

func hello(id int) {
    fmt.Printf("Hello from Goroutine %d\n", id)
    time.Sleep(100 * time.Millisecond)
}

func main() {
    for i := 1; i <= 5; i++ {
        go hello(i) // 启动一个新的 Goroutine
    }
    time.Sleep(200 * time.Millisecond) // 主 Goroutine 等待
}
  • go hello(i) 会在运行时创建一个新的 Goroutine 节点 G,放入可运行队列,等待被 P 调度。
  • 初始栈仅 2KB,足够普通函数调用,当栈空间不足时,运行时会自动将栈扩展为 4KB、8KB……直至最大 1GB 左右。

2.1.2 栈扩展与收缩

Go 运行时为每个 G 维护一个栈段(stack),并且会通过“分段复制”实现动态扩展。大致流程如下:

  1. Goroutine 首次运行时,运行在一块很小的栈(2KB);
  2. 当函数调用深度/局部变量导致栈溢出阈值时,运行时会申请一块更大的栈(例如 4KB),并把旧栈中的数据复制到新栈;
  3. 栈扩展过程对程序透明,不需开发者手动干预;
  4. 当栈空间空闲率较高时,运行时也会将栈收缩回更小的尺寸,以节省内存。

Mermaid 图解:栈扩展示意

sequenceDiagram
    participant Goroutine as G
    Note over Goroutine: 初始栈(2KB)
    Goroutine->>Runtime: 递归调用或大局部变量分配
    Runtime->>Runtime: 检测栈空间不足
    Runtime->>NewStack: 分配更大栈(4KB)
    Runtime->>OldStack: 将旧栈数据复制到新栈
    Note over Goroutine: 继续执行在新栈上(4KB)

2.2 Goroutine 调度与状态机

每个 Goroutine 有一个 状态机,常见状态包括:

  • Gwaiting:等待被调度;
  • Grunnable:已准备好,可在本地队列或全局队列中排队等待;
  • Grunning:正在某个 P 上执行;
  • Gsyscall:执行系统调用时,脱离 P,自行解绑(用于非阻塞);
  • Gblocked:等待 Channel、select、锁等同步原语;
  • Gdead:执行完毕或 panic 回收。

Goroutine 状态机示意图

flowchart LR
    Gwaiting --> Grunnable
    Grunnable --> Grunning
    Grunning --> Gsyscall
    Grunning --> Gblocked
    Gsyscall --> Grunnable
    Gblocked --> Grunnable
    Grunning --> Gdead
  • 当一个 Goroutine 需要做 Channel 发送/接收同步原语阻塞,会进入 Gblocked
  • 当调用了 系统调用(如 net.Listenos.Open)时,会进入 Gsyscall,在此期间释放 P,以让其他 Goroutine 运行;
  • 任何可以继续执行的状态一旦准备就绪,就会进入 Grunnable,等待 P 调度到 CPU。

3. Processor(P)详解

3.1 P 的角色与数量控制(GOMAXPROCS)

  • P(Processor) 表示 Go 运行时调度 Goroutine 的上下文容器,相当于“逻辑 CPU”资源。
  • 每个 P 拥有一个本地 run queue(队列),用于存放可运行的 Goroutine(G)。
  • 在 Go1.5 之后,默认 GOMAXPROCS 为系统 CPU 核数,也可以通过 runtime.GOMAXPROCS(n) 动态设置。
  • 运行时会创建 P 个 M(Machine,即 OS 线程)与之匹配,确保同时只有 P 个 Goroutine 真正运行在 CPU 上。
import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println("默认 GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 0 表示获取当前值
    // 设置为 2
    runtime.GOMAXPROCS(2)
    fmt.Println("修改后 GOMAXPROCS:", runtime.GOMAXPROCS(0))
}
  • 设置 GOMAXPROCS = 2 意味着同时最多有 2 个 Goroutine 在真正运行(并行)于 CPU;
  • 如果有更多 Goroutine 处于 Grunnable,则会排队在 P 本地队列或全局队列,等待下一次调度。

3.2 本地队列与全局队列

  • 每个 P 有一个 local run queue,长度默认为 256,存储当前逻辑处理器归属的 Runnable Goroutine;
  • 如果本地队列已满,新的可运行 Goroutine 会被推到 全局队列
  • 当某个 P 的本地队列空了时,会尝试从全局队列拉取或者从其他 P 的本地队列进行 工作窃取(Work Stealing),确保负载均衡。

Mermaid 图解:P 与本地/全局队列

flowchart LR
    subgraph P1[Processor P1]
        R1[Runnable Gs (本地队列)]
    end
    subgraph P2[Processor P2]
        R2[Runnable Gs (本地队列)]
    end
    subgraph Global[全局队列]
        Q[所有 P 的溢出任务]
    end
    subgraph M[Machines]
        M1[M1 (线程)] -->|调度| P1
        M2[M2 (线程)] -->|调度| P2
    end
    P1 --> R1
    P2 --> R2
    R1 --> Q
    R2 --> Q
  • 当 G 由 Goroutine 创建成为 Grunnable 时,会首先进入创建时所在的 P 的本地队列;
  • 若本地队列已满,才会推到全局队列;
  • P 在执行完成自己的本地队列后,会尝试从全局队列拉取或者向其他 P 窃取。

4. Machine(M)详解

4.1 M 对应操作系统线程

  • M(Machine) 表示一个真正的操作系统线程(OS Thread);
  • M 与 P 绑定后,就代表一个 OS 线程上运行某个 P 的调度循环,并执行对应的 Goroutine;
  • 当某个 P 上的 Goroutine 发起系统调用(如 I/O、文件操作等)时,该 M 会进入阻塞状态,从而会 解绑 P,让 P 去另一个空闲的 M 上运行,以避免整个线程阻塞影响其他 Goroutine 的执行。

Mermaid 图解:M 与 P/G 关系

flowchart LR
    subgraph M1[OS Thread M1]
        P1[Processor P1] --> G1[Goroutine G1]
        P1 --> G2[Goroutine G2]
    end
    subgraph M2[OS Thread M2]
        P2[Processor P2] --> G3[Goroutine G3]
    end
  • M1 绑定 P1,P1 再调度 G1、G2;
  • M2 绑定 P2,P2 再调度 G3。

4.2 系统调用与 M 的阻塞/唤醒

当 Goroutine 执行系统调用时,运行时会执行以下逻辑:

  1. Goroutine 状态切换为 Gsyscall,此时它不在任何 P 的本地队列;
  2. 该 M 与当前 P 解绑,M 单独去执行系统调用直到完成;
  3. 当前 P 发现 M 被解绑后,会将自己标记为“可用”,并尝试去绑定其他可用 M 或者创建一个新 M;
  4. 当系统调用返回后,被阻塞的 M 会将 Goroutine 状态切换回 Grunnable,再重新放入本地队列等待下一次调度。

Mermaid 图解:系统调用 & M 解绑示意

sequenceDiagram
    participant G as Goroutine
    participant M as OS Thread M
    participant P as Processor P

    G->>G: 执行系统调用(如文件读写)
    G->>M: Gsyscall 标记, M 与 P 解绑
    M->>OS: 执行系统调用, 阻塞
    P->>P: P 解绑后标记可用,寻找其他 M 绑定
    Note over P:  P 可绑定新的 M,继续调度其他 G
    OS-->>M: 系统调用完成
    M->>G: 标记 G 为 Grunnable
    G->>P: G 重新进入本地队列

5. GPM 调度器协作流程

在 Go 运行时中,G、P、M 三者相互配合,通过以下几个关键机制实现高效并发调度。

5.1 工作窃取(Work Stealing)

当某个 P 的本地队列耗尽时,它会尝试从其他 P 那里“窃取”一部分可运行的 G,以避免空闲资源浪费。窃取的策略大致如下:

  1. 当 P1 的本地队列阈值低于某个预定值,从全局队列或随机其他 P 的本地队列尝试窃取一半左右的任务;
  2. 窃取到的 G 放入 P1 的本地队列,M1(绑定 P1 的线程)继续执行;
  3. 如果实在没有可窃取的任务,P1 会进入空闲状态,等待未来有 Goroutine 变为可运行时重新唤醒。

Mermaid 图解:工作窃取示意

sequenceDiagram
    participant P1 as Processor P1
    participant P2 as Processor P2
    participant Global as 全局队列

    P1-->>P1: 本地队列为空
    P1->>Global: 尝试从全局队列拉取任务
    Global-->>P1: 提供部分任务
    P1-->>M1: 调度并执行任务

    P2-->>P2: 本地队列很多任务
    Note over P1,P2: 如果 Global 也为空,则 P1 尝试向 P2 窃取
    P1->>P2: 窃取部分任务
    P2-->>P1: 返回部分任务
    P1-->>M1: 调度执行

5.2 调度器循环与抢占

Go 运行时为每个 P 维护一个调度循环(schedule loop),大致逻辑为:

for {
    // 1. 获取本地队列头部的 Goroutine G
    g := runQueuePop(p)
    if g == nil {
        // 2. 本地队列空:尝试窃取 or 全局队列取
        g = getWork(p)
    }
    if g == nil {
        // 3. 真正空闲:进入空闲状态
        p.idle()
        continue
    }
    // 4. 将当前 P 绑定到 M 上,执行 G
    m := acquireM(p)
    m.g0 = g
    m.run() // 运行在 M 绑定的 OS 线程上
    // 5. 执行完成后,G 可能变为 Grunnable,又要再次加入队列
}

5.2.1 Goroutine 抢占

在 Go 1.14 之后引入了 Goroutine 抢占机制,通过在函数调用链上注入抢占点,使长期运行的计算型 Goroutine 可以在适当时机被抢占,让出 CPU 给其他 Goroutine。抢占逻辑概览:

  • 编译器会在一些函数调用或循环旁插入 runtime·goschedguarded 标记,并在预定 GC 周期或系统调用返回时触发抢占;
  • 当需要抢占时,运行时会将目标 Goroutine 标记为 Gpreempted,然后在该 Goroutine 下次安全点断点时切换上下文;
  • 损失较小的性能开销即可实现更公平的调度,防止长时间计算 Goroutine 独占 CPU。

5.3 阻塞与唤醒示例

当 Goroutine 在 Channel 接收、锁等待或系统调用时进入阻塞,运行时会进行如下操作:

  1. 将 G 置为 GwaitingGsyscall,从 P 的本地队列移除;
  2. 如果是系统调用,则 M 与 P 解绑;
  3. 等待条件满足后(如 Channel 有数据、锁被释放、系统调用返回),将 G 标记为 Grunnable 并放入某个 P 的本地队列;
  4. 唤醒相应的 M 以继续调度。

示例:Channel 接收阻塞唤醒

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    go func() {
        fmt.Println("子 Goroutine: 等待 1s 后发送数据")
        time.Sleep(1 * time.Second)
        ch <- 42 // 发送者唤醒阻塞在接收处的 Goroutine
    }()
    fmt.Println("主 Goroutine: 阻塞等待接收")
    v := <-ch
    fmt.Println("主 Goroutine: 收到", v)
}
  • 主 Goroutine 在 <-ch 阻塞,会被置为 Gblocked,从 P 的本地队列移除;
  • 1 秒后,子 Goroutine ch <- 42 会将数据放入缓冲区,并唤醒主 Goroutine;
  • 主 Goroutine 标记为 Grunnable,等待当前 P 调度继续执行。

6. 代码示例:并发调度演示

为了更直观地理解 GPM 的调度行为,我们通过几个示例演示 Goroutine 调度与并行度控制。

6.1 简单高并发 Goroutine 示例

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Goroutine %d 开始执行,绑定到 P: %d\n", id, runtime.GOMAXPROCS(0))
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("Goroutine %d 执行完毕\n", id)
}

func main() {
    // 设置 GOMAXPROCS 为 2
    runtime.GOMAXPROCS(2)
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }
    wg.Wait()
    fmt.Println("所有 Goroutine 完成")
}
  • runtime.GOMAXPROCS(2) 设置 P 数量为 2,意味着同时最多有 2 个 Goroutine 并发执行;
  • 虽然启动了 5 个 Goroutine,但它们会排队在 2 个 P 上执行,并分批完成。

6.2 利用 GOMAXPROCS 调整并行度

通过调整 GOMAXPROCS,可以观察程序在不同并行度下的执行时间差异:

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func busyLoop(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    sum := 0
    for i := 0; i < 1e7; i++ {
        sum += i
    }
    fmt.Printf("Goroutine %d 计算完成, sum=%d\n", id, sum)
}

func benchmark(n int) {
    var wg sync.WaitGroup
    start := time.Now()
    for i := 1; i <= n; i++ {
        wg.Add(1)
        go busyLoop(i, &wg)
    }
    wg.Wait()
    fmt.Printf("GOMAXPROCS=%d, 启动 %d 个 Goroutine 所需时间: %v\n", runtime.GOMAXPROCS(0), n, time.Since(start))
}

func main() {
    for _, procs := range []int{1, 2, 4} {
        runtime.GOMAXPROCS(procs)
        benchmark(4)
    }
}

可能输出示例

GOMAXPROCS=1, 启动 4 个 Goroutine 所需时间: 500ms
GOMAXPROCS=2, 启动 4 个 Goroutine 所需时间: 300ms
GOMAXPROCS=4, 启动 4 个 Goroutine 所需时间: 250ms
  • 随着 GOMAXPROCS 增加,多个 Goroutine 可并行执行,整体耗时明显下降;
  • 但当数量超过 CPU 核数时,可能涨幅变小或持平,因为上下文切换成本上升。

6.3 结合 runtime 包探查 GPM 状态

Go 提供了一些函数来获取运行时的调度信息,如 runtime.NumGoroutine()runtime.GOMAXPROCS() 等。

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func sleepWorker(wg *sync.WaitGroup) {
    defer wg.Done()
    time.Sleep(500 * time.Millisecond)
}

func main() {
    runtime.GOMAXPROCS(2)
    var wg sync.WaitGroup

    fmt.Println("启动前 Goroutine 数量:", runtime.NumGoroutine()) // 通常是 1(main + 系统线程)

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go sleepWorker(&wg)
    }
    fmt.Println("启动后 Goroutine 数量:", runtime.NumGoroutine()) // 应该是 6(1 main + 5 睡眠中的)

    wg.Wait()
    time.Sleep(100 * time.Millisecond) // 等待调度完成
    fmt.Println("完成后 Goroutine 数量:", runtime.NumGoroutine()) // 应回到 1
}
  • 通过 NumGoroutine()GOMAXPROCS() 可以了解当前 Goroutine 数量与 P 数量;
  • 这有助于在调试调度问题时快速确定系统状态。

7. Mermaid 图解:GPM 调度流程

下面通过多个 Mermaid 图表,将 GPM 模型的调度核心流程可视化,帮助你快速理解各种情况下的切换逻辑。

7.1 Goroutine 创建与排队

flowchart TD
    subgraph main Goroutine
        M0[M0 Thread]
        Gmain[主 Goroutine]
    end
    Gmain -->|go f()| [*]CreateG[创建新 Goroutine G1]
    CreateG -->|加入 P1 本地队列| P1[Processor P1 本地队列]
    P1 --> M1[M1 Thread 负责 P1]
    M1 -->|调度| G1[Goroutine G1]
  • 主 Goroutine 通过 go f() 创建 G1,G1 放入 P1 的本地队列;
  • M1(线程)绑定到 P1,从本地队列中取出 G1 并执行。

7.2 本地队列耗尽后的工作窃取

flowchart LR
    subgraph P1[Processor P1]
        Loc1[空] 
    end
    subgraph P2[Processor P2]
        Loc2[多任务队列 (G2, G3, G4)] 
    end
    subgraph Global[全局队列]
        Glob[若存在溢出任务]
    end
    P1 -->|本地队列空| P1Steal[尝试从全局/其他 P 窃取]
    P1Steal -->|从 P2| Steal[G2, G3]
    Steal --> P1
    P2 -->|剩余 G4| 执行...
  • 当 P1 本地队列空时,P1 会先检查全局队列,如无则尝试向 P2 窃取若干 Goroutine;
  • 窃取后 P1 执行这些任务,保持并行度。

7.3 系统调用阻塞与 M 解绑

sequenceDiagram
    participant G as Goroutine G1
    participant P as Processor P1
    participant M as Machine M1 (OS Thread)
    participant OS as 操作系统

    G->>M: 执行系统调用(如文件读写)
    M->>OS: 切换到内核模式,阻塞
    M-->>P: 通知 P 解绑 (P 可重新绑定其他 M)
    P->>P: 寻找新的 M 绑定
    OS-->>M: 系统调用返回
    M->>G: G 从 Gsyscall 状态变为 Grunnable
    G->>P: G 放入 P 本地队列,等待调度
  • 当 G1 执行系统调用,会使 M1 阻塞并与 P1 解绑,使 P1 可继续调度其他 G;
  • 系统调用返回后,M1 会将 G1 标记为 Grunnable 并重新放入调度队列。

8. 调优与常见问题

在了解 GPM 模型后,在实际项目中仍需注意以下几个方面的调优与常见陷阱:

8.1 GOMAXPROCS 设置不当

  • 设置过小:会导致并发 Goroutine 在少数 P 上排队,真正并行度不足;
  • 设置过大:如果 GOMAXPROCS 大于 CPU 核数,反而增加线程切换和缓存抖动开销,可能降低性能。
  • 一般推荐设置为 runtime.NumCPU(),对于 I/O 密集型应用可适当提高 1\~2 个 P,但需结合具体性能测试。

8.2 阻塞型系统调用

  • 如果 Goroutine 频繁进行长时间阻塞的系统调用(如文件 I/O、网络 I/O),会产生大量 M 与 P 解绑/重绑,增大调度和线程管理开销;
  • 推荐将 I/O 操作尽量设计为异步或使用 Go “非阻塞网络 I/O”+ epoll 的方式,让运行时有效管理。
  • 避免在热路径中调用 time.Sleep 等阻塞操作,可使用 time.AfterFunccontext.WithTimeout 等更灵活的控制方式。

8.3 Goroutine 泄露与队列饱和

  • 未能及时关闭或退出的 Goroutine 会长期占用资源,造成泄露;
  • 本地队列或全局队列过度堆积可导致调度变慢,增加抢占开销;
  • 建议对生产 Goroutine 的场景(如无限循环的 Worker Pool)设计退出信号context.Cancelclose(ch)),并定期检查队列长度 runtime.NumGoroutine()
// 监控 Goroutine 泄露示例
go func() {
    for {
        fmt.Println("当前 Goroutine 数量:", runtime.NumGoroutine())
        time.Sleep(1 * time.Second)
    }
}()

9. 小结

本文从 GPM 模型 的三个核心组件——Goroutine(G)、Processor(P)、Machine(M)入手,详细解析了 Go 运行时如何通过工作窃取本地/全局队列系统调用阻塞与 M 解绑Goroutine 栈扩展等机制,高效地管理数以万计的 Goroutine 并行执行。通过代码示例与 Mermaid 图解,相信你已经对 Go 并发底层调度有了更清晰的认识。

  • 理解 Goroutine 的轻量与动态栈扩展,有助于在项目中大胆地创建大量并发任务;
  • 合理设置 GOMAXPROCS,才能发挥多核优势,同时避免过度抢占开销;
  • 关注阻塞型系统调用带来的 M/P 解绑成本,尽量使用 Go 原生的异步 I/O;
  • 结合 runtime 包的监控接口,及时发现并解决 Goroutine 泄露与队列饱和问题。

掌握 GPM 模型,对于构建高并发、低延迟的 Go 应用至关重要。

最后修改于:2025年06月04日 11:08

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日