协程跟 cpu 有关系吗?

2020-12-03 20:56:01 +08:00
 vevlins

我觉得啥关系也没有。

通过 cps 实现 call/cc->通过 call/cc 实现协程。以我的理解,cps 就是编译器层面的自动 callback,纯粹是语言层面的东西,只要能够做 callback->也就是能够 JMP->也就是移动程序指针->也就是图灵机的最基本要求,就可以实现协程,整个实现过程跟 cpu 没什么关系。

在 C/C++语言中有什么区别吗?我是按照 JS 的知识分析的。

4162 次点击
所在节点    程序员
31 条回复
icexin
2020-12-03 23:11:13 +08:00
callback 函数不是一个 jmp 指令那么简单,调用者需要传参,保存返回的 PC 指针,有时甚至要保存[caller-saved-registers]( https://stackoverflow.com/questions/9268586/what-are-callee-and-caller-saved-registers);被调用者需要保存调用者的栈帧以方便在函数返回的时候恢复之前的栈帧。这些都是跟具体 CPU 指令相关的,我们没感觉是因为编译器帮按照语言的语义帮抹平了不同 CPU 的差异。
对于 go 这样的每个 goroutine 有自己独立栈的,在切换 goroutine 的时候还需要切换对应的栈寄存器。
no1xsyzy
2020-12-03 23:44:38 +08:00
@lewis89 不是,连指令集都不用,你这个还依赖三个寄存器呢,实际上还依赖硬件底层提供了访问栈帧的方式。
CPS ( Continuation-passing style )就是回调地狱的那个形式,不是特定指令集的名字。不过这个复杂的回调是从任意代码生成的。
(论传统,X86 比不过 call/cc 的吧…… 那个是在 lisp machine 上就实现了的……

想了想,应该这么说,你这个是在 CPU 上直接套一层实现协作式调度。而楼主的想法是在编译器层面实现的协作式调度。
我完全可以不运行用户篡改栈帧,同时写出一个 Python 解释器并使用 Python 那套协程。我所知,stackless 的协程更多是倾向楼主的想法的,依赖 call/cc 的一个下位替换叫 generator 。
katsusan
2020-12-03 23:47:49 +08:00
可以参考 下 golang 的协程上下文:
struct runtime.gobuf {
uintptr sp;
uintptr pc;
runtime.guintptr g;
void *ctxt; // 闭包函数首地址,x86_64 下存在 DX 寄存器
runtime/internal/sys.Uintreg ret; // 系统调用返回值
uintptr lr; // ARM 下的 link register
uintptr bp;
}
以 X64 为例,切换上下文的时候大概这样执行:
MOV gobuf_sp RSP;
MOV gobuf_bp RBP;
MOV gobuf_ctxt RDX;
MOV gobuf_ret RAX;
MOV gobuf_pc RBX;
JMP *RBX;
lewis89
2020-12-03 23:50:41 +08:00
@no1xsyzy #22 我找了 CPS 的中文文章 看了老半天 也没看明白.. 可能我只能理解命令式的编程方式吧
laminux29
2020-12-04 00:15:43 +08:00
进程太重了出线程,线程太重了出协程,仅此而已。

题主可以去翻翻操作系统演变史,当年在有了进程的情况下,为什么要创造出线程,以及进程与线程的差异。
CismonX
2020-12-04 00:25:13 +08:00
call/cc 中的 continuation 并不是协程,前者可以用来实现后者,但后者不能用来实现前者

有关 continuation 的多种不同实现,这篇简单的文章有介绍: https://wiki.c2.com/?ContinuationImplementation

至于 C++ 中的实现,举几个例子:boost::context 用了 segmented stack 实现 continuation,而以 boost::asio::yield 为典型的 stackless coroutine 是将状态存储到一个变量中,然后在语言层面利用类似 duff's device 的特性做了 hack,实现了 yield 语法。

如果说和 CPU 的关系,无非是可以利用某些特定指令来对 continuation 的实现做优化,我觉得并没有必然关系。在任何图灵完备的系统上理论上都可以实现 continuation

支持 call/cc 的编程语言中,语法最简单的是 Unlambda 语言。它也是一个非常有趣的语言。我写过它的两个实现:一个是正统的 C 实现( https://github.com/esolangs/u6a ),用 segmented stack 实现 continuation ;另一个是基于 TypeScript 的类型系统实现的( https://github.com/esolangs/type-unlambda ),使用 CPS 实现 continuation 。
no1xsyzy
2020-12-04 00:27:21 +08:00
@lewis89 …… 淦咧你到底看了什么…… CPS 分明是函数式一系的……
想了想我怎么解释都不如 wiki 清楚,而且 v2 还没格式…… https://en.wikipedia.org/wiki/Continuation-passing_style

命令式编程的控制流:一句运行完运行下一句。
而 CPS,把运算结果的去向传递给函数,让函数自己去 call 这个去向。
vevlins
2020-12-04 09:33:26 +08:00
收获很多,学习学习
xhystc
2020-12-04 10:26:29 +08:00
协程其实就是操作系统这门课所说的用户级线程换了个名词而已,我记得哈工大操作系统公开课有一节讲的是用户级线程的实现,那个就是协程的基本原理,本质就是用户空间的堆栈和 cpu 上下文的切换,没必要引入那么多名词和概念
no1xsyzy
2020-12-04 12:30:37 +08:00
@lewis89 刚在提醒系统里看到你这句我发现我眼花看错了……
“我只能理解命令式的编程方式吧” 我给看成了 “我只能理解**为**命令式的编程方式吧” (手动笑哭

…… 那撇开我上面说的那些,考虑下 go channel 和 pony / erlang 的消息。
更现实地,可以考虑 “流程图”,为每根线标上一个名字,这样的话一个执行节点上运行完,直接告诉调度器 “我的下游是 XX 这根线”。
其实这每根线就是命名续延。
secondwtq
2020-12-04 13:27:47 +08:00
这评论都什么乱七八糟的 ...

C++ 最新的协程标准应该是 stackless 的,根据一个微软的家伙吹的,是所有协程中最 flexible,最 powerful blabla 的(原话忘了)
C 的协程一般都是 stacked 的

楼主的理解大致没啥问题,你把这东西放编译器上就是 stackless coroutine,放汇编里就是 stacked coroutine,放内核里就是线程

至于什么是“CPU”的“关系”,我认为楼主的意思是一个 CPU 必须要提供一套能做到图灵完全的基本操作,超过这一范围的就属于“guanxi”。从这个角度来说,函数调用,补码,浮点运算,向量运算,虚拟内存等,甚至是基本的整数运算,都是现代 CPU 的“关系”。John Gustafson 没有关系,所以 posit 没戏。

另外反正评论区已经足够群魔乱舞了:
这东西其实就是先让一部分计算执行完后,再执行什么计算的问题,所以我认为协程是一个中国特色社会主义理论问题

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://tanronggui.xyz/t/731898

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX