惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
B
Blog
P
Proofpoint News Feed
美团技术团队
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
A
About on SuperTechFans
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Vercel News
Vercel News
有赞技术团队
有赞技术团队
小众软件
小众软件
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
aimingoo的专栏
aimingoo的专栏
H
Help Net Security
罗磊的独立博客
L
LangChain Blog
GbyAI
GbyAI
腾讯CDC
T
The Blog of Author Tim Ferriss
Microsoft Security Blog
Microsoft Security Blog

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例)
协程(coroutine)简介 - 什么是协程?
Jiajun Huang · 2018-04-03 · via Jiajun的技术笔记

如果涉及到具体的操作系统和CPU等,则为 Linux/amd64

程序内存布局

program memory layout

首先介绍一下内存布局,从高地址到低地址,依次是:

  • kernal space
  • stack
  • heap
  • BSS segment(未初始化或者为0值的的全局或者static变量)
  • data segment(显式初始化的全局变量或者static变量)
  • text segment(也叫code segment,存储的是具体的指令)

当然,这个还不完全,还有很多段没有列出来例如stack和heap中间可能有mmap的内存段。此外此处还没有考虑到多线程的情况下,不过为了热热身,已经够了。

栈和帧

栈的作用主要是用来保存函数调用之后的该返回哪里。通常,调用一个函数,进行的步骤有把参数从右向左依次压栈, 然后把返回地址压栈,然后开始执行函数。当然,这是实现细节,只是目前比较常用的一种方式。当函数执行完之后,就取出返回地址, 继续执行。有一个寄存器,PC(Program Counter)就是用来存储下一条要执行的指令的。

计算机中有一句名言,所有的问题都可以引入一个中间层来解决,那么,为什么会有帧呢?解决了什么问题?我个人的看法是,将多个 函数调用分别包装起来,从而进行了隔离,当然,这里的“包装”是一个抽象概念,实际上,只要你想,仍然可以访问到其他函数的栈 内的内容。

call stack layout

协程(coroutine)

无栈协程(stackless coroutine)

Python中的协程一般是通过yield来实现,有yield的函数,其返回结果就是一个generator。而Python中的协程就属于无栈协程。 那我们来看一下generator object长啥样:

/* _PyGenObject_HEAD defines the initial segment of generator
   and coroutine objects. */
#define _PyGenObject_HEAD(prefix)                                           \
    PyObject_HEAD                                                           \
    /* Note: gi_frame can be NULL if the generator is "finished" */         \
    struct _frame *prefix##_frame;                                          \
    /* True if generator is being executed. */                              \
    char prefix##_running;                                                  \
    /* The code object backing the generator */                             \
    PyObject *prefix##_code;                                                \
    /* List of weak reference. */                                           \
    PyObject *prefix##_weakreflist;                                         \
    /* Name of the generator. */                                            \
    PyObject *prefix##_name;                                                \
    /* Qualified name of the generator. */                                  \
    PyObject *prefix##_qualname;

typedef struct {
    /* The gi_ prefix is intended to remind of generator-iterator. */
    _PyGenObject_HEAD(gi)
} PyGenObject;

为什么叫做无栈协程呢?因为generator没有自己的栈,只有它自己的帧。每次执行到这个coroutine的时候,是把其帧丢到调用者的 栈上的。

有栈协程(stackful coroutine)

而Go的协程则属于有栈协程,在这篇文章里有说到 具体切换的时候,涉及到哪些东西。我在这里再列出来一下,我们看看G长啥样:

type g struct {
        // Stack parameters.
        // stack describes the actual stack memory: [stack.lo, stack.hi).
        // stackguard0 is the stack pointer compared in the Go stack growth prologue.
        // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
        // stackguard1 is the stack pointer compared in the C stack growth prologue.
        // It is stack.lo+StackGuard on g0 and gsignal stacks.
        // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
        stack       stack   // offset known to runtime/cgo
        stackguard0 uintptr // offset known to liblink
        stackguard1 uintptr // offset known to liblink

        _panic         *_panic // innermost panic - offset known to liblink
        _defer         *_defer // innermost defer
        m              *m      // current m; offset known to arm liblink
        sched          gobuf
        ...
}

其中的gobuf就是协程中保存的东西,我们看看:

type gobuf struct {
	// The offsets of sp, pc, and g are known to (hard-coded in) libmach.
	//
	// ctxt is unusual with respect to GC: it may be a
	// heap-allocated funcval, so GC needs to track it, but it
	// needs to be set and cleared from assembly, where it's
	// difficult to have write barriers. However, ctxt is really a
	// saved, live register, and we only ever exchange it between
	// the real register and the gobuf. Hence, we treat it as a
	// root during stack scanning, which means assembly that saves
	// and restores it doesn't need write barriers. It's still
	// typed as a pointer so that any other writes from Go get
	// write barriers.
	sp   uintptr
	pc   uintptr
	g    guintptr
	ctxt unsafe.Pointer
	ret  sys.Uintreg
	lr   uintptr
	bp   uintptr // for GOEXPERIMENT=framepointer
}

把好几个重要的寄存器的值全部保存了,如 SP, PC等。其实就是把整个G的栈全部保存下来,换成新的要执行的G的栈的值。

对比

stackful coroutine的优势在于可以在栈执行到任意一步的时候端走,而stackless则只能端走栈中最顶部的那个frame。 如果要说stackful coroutine的缺点的话,我觉得应该是实现比较复杂,goroutine的实现还算简单,是因为它没有用到所有的 寄存器,只用到栈底,栈顶,framepointer,PC等几个。例如传参数,都是统一往栈里丢,当然,它这样实现也是为了简单。 一般的C程序,都是前几个参数丢某某寄存器,更多的参数才丢栈里。


参考资料: