LemonHX

LemonHX

CEO of Limit-LAB 喜欢鼓捣底层的代码,意图改变世界
twitter
tg_channel

Go 语言赢两次并遥遥领先

Goroutines 是 Go 语言中用于实现协作式多任务处理的机制,使得 Go 成为一种既简单又高效的并发编程语言。

最关键的是这东西可比教给小白什么是 Async 方便多了

大家别没事儿找事儿反驳我 Go 不好学,我昨天刚刚让一个完全没有 Go 经验的人在两个小时内学会了 Go

在 Go 中,每个 goroutine 都拥有自己的独立栈,可以存储临时值和返回地址等信息。事实上,Go 编译器在每个 goroutine 开始时都会分配一个特别小的栈,并根据需要进行扩展。

下面我就来解释下为什么 go 和 async 其实本质上在做一个事情

我们拿 Go 语言的一个实例 TinyGo 对 goroutines 的实现为例。它基于类似于 C#、JavaScript 和现在也用于 C++ 的异步 / 等待(async/await)模型。事实上,TinyGo好像看起来会自动插入 async/await 关键字,因为 TinyGo 是单线程的。

让我们以以下代码片段作为示例:

func main() {
	go background()
	time.Sleep(2 * time.Second)
	println("some other operation")
	n := compute()
	println("done:", n)
}

func background() {
	for {
		println("background operation...")
		time.Sleep(time.Second)
	}
}

func compute() int {
	time.Sleep(time.Second)
	println("blocking operation completed")
	return 42
}

如果我们人肉给他加上 async await 换成 JS 人可能会更熟悉的语法

async func main() {
	go background()
	await time.Sleep(2 * time.Second)
	println("some other operation")
	n := await compute()
	println("done:", n)
}

async func background() {
	for {
		println("background operation...")
		await time.Sleep(time.Second)
	}
}

async func compute() int {
	await time.Sleep(time.Second)
	println("blocking operation completed")
	return 42
}

我相信绝大多数用户到这里还没有问题,但是别急,我们真的不需要学 async await。

我要引入 Delimited Continuation 了#

如果看不懂请查看文章 Delimited Continuation

Delimited Continuation(被限定延续)是一种编程模型,用于控制程序的执行流程,并在需要的时候保存和恢复执行的状态。它提供了一种灵活而强大的控制流转移机制,可以减轻开发者的心智负担。

在传统的控制流中,函数的执行顺序是线性的,每个函数都有一个入口和一个出口。当一个函数调用另一个函数时,控制权会转移到被调用函数,当被调用函数完成时,控制权返回给调用函数。这种线性控制流并不适用于一些复杂的编程需求,例如在异步编程中的回调地狱、协程的实现等。

被限定延续允许在函数的任意位置保存当前的执行状态,并将控制权转移到另一个代码片段。这个代码片段被称为延续,它可以是一个闭包、一个函数或其他可执行的代码块。被限定延续提供了一种将执行状态携带到不同的代码片段的能力,从而实现了非局部的控制流转移。

与被限定延续相关的是 Async Await 模型,它是一种用于简化异步操作的编程模型,广泛应用于现代异步编程。Async Await 允许开发者以同步的方式编写异步代码,而无需显式地处理回调函数。它通过暂停和恢复函数的执行来等待异步操作的完成,在异步操作完成后继续执行后续的代码。

被限定延续和 Async Await 之间存在关联和共享的概念。Async Await 实际上是基于被限定延续实现的异步编程模型。当使用 Await 暂停异步函数时,实际上是将当前的执行状态保存到被限定延续中,并在异步操作完成后恢复执行。

利用被限定延续和编译器自动实现转换的好处是能够大大减轻开发者的心智负担。通过自动处理被限定延续的转换,开发者不需要手动处理复杂的控制流操作,使得代码更加简洁、可读性更高。这种转换的自动化能够提供更高级别的抽象,让开发者专注于业务逻辑而不是底层的控制流细节。

自动处理被限定延续转换的好处还包括:

  1. 简化异步编程: 使用被限定延续可以简化异步编程模型,使得编写异步代码更加直观和易于理解。开发者可以像编写同步代码一样使用异步功能,而不再需要处理复杂的回调函数和嵌套。

  2. 减少错误和提高可维护性: 自动处理被限定延续转换可以减少编码中的错误,并提高代码的可维护性。由于这种转换是由编译器自动完成的,因此不容易出现手动处理控制流问题时常见的错误。

  3. 简化复杂的控制流: 被限定延续提供了更灵活和细粒度的控制流转移,使得处理复杂的控制流模式变得更容易。开发者可以使用被限定延续实现更高级别的控制结构,如协程、状态机等。

大家反应过来没有#

Go 语言最大的优点是整个语言成线性,没有奇葩的控制流(假设你不 recover),整个语言就一个并发手段也就是 go 那么,我们是不是很简单的就能把他转为 delimited continuation 格式呢?

假设我的编译器有一个 pass 可以将 go 代码根据控制流转换成 delimited continuation 格式

这个 pass 非常简单,因为从 ast 转换为 cps 形式的 ast 我们淫王老师 100 行代码就能搞定

我们会得到代码

func main() {
    background(func() {
        time.Sleep(2 * time.Second)
        println("some other operation")
        compute(func(n int) {
            println("done:", n)
        })
    })
}

func background(k func()) {
    for {
        println("background operation...")
        time.Sleep(time.Second)
        k()
    }
}

func compute(k func(int)) {
    time.Sleep(time.Second)
    println("blocking operation completed")
    k(42)
}

然后再通过一个 pass 把这种 trivial 的 delimited continuation 翻译到 LLVM 的 coroutine

感谢 C 艹老铁不断堆特性,让 LLVM 有了这么好用的功能

func main(parent *coroutine) {              // note: parent is always nil because this is main
  hdl := llvm.makeCoroutine()             // my coroutine
  background(nil)                         // not passing a parent as it is a new independent goroutine
  runtime.sleepTask(hdl, 2 * time.Second) // mark this function as sleeping
  llvm.suspend(hdl)                       // suspend this coroutine
  println("some other operation")
  compute(hdl)                            // continuation-passing style
  llvm.suspend(hdl)
  n := hdl.data
  println("done:", n)
  runtime.resumeTask(parent)              // re-activate parent (unnecessary)
}

func background(parent *coroutine) {
  hdl := llvm.makeCoroutine()
  for {
    println("background operation...")
    runtime.sleepTask(hdl, time.Second) // mark this function as sleeping
    llvm.suspend(hdl)
  }
// code is unreachable so there is no runtime.resumeTask call.
}

func compute(parent *coroutine) {
  hdl := llvm.makeCoroutine()
  runtime.sleepTask(hdl, time.Second) // mark this function as sleeping
  llvm.suspend(hdl)
  println("blocking operation completed")
  parent.data = 42                    // note: not yet implemented
  runtime.resumeTask(parent)          // re-activate parent
}


然后我们最后把 background 的 hdl spawn 在某个 task queue 就完成了整个过程

为什么我们说 Go 赢了两次?#

转换为 CPS 的 delimited continuation 困难程度:#

转换为 CPS 的 delimited continuation 将整个程序转换为以延续函数形式传递控制流。Go 语言中,只有go函数和<-两个个控制流操作,因此将代码转换为 CPS 风格相对较简单。

实现自动染色的 Async Await 困难程度:#

实现自动染色的 Async Await 需要考虑编译器和运行时环境的复杂性。这种自动化转换需要编译器对代码进行静态分析和转换,将异步操作标记为挂起点,并在适当的时候恢复执行。编译器需要进行语法分析、语义分析以及生成相应的中间表示或目标代码。

实现自动染色的 Async Await 具有较高的技术复杂度。这个 pass 需要处理复杂的控制流、代码生成和优化问题。这也需要对编译器和底层语言的运行机制有深入的理解。

运行时消耗的资源情况#

在运行时,自动染色的 Async Await 模型通常需要维护额外的状态,如异步任务的状态、挂起点的上下文等。这些状态可能需要额外的内存和处理开销,并增加运行时的复杂性。同时,执行异步操作可能需要调度和管理协程或线程的运行,涉及到上下文切换和调度器的开销。

然后我们感谢 C 艹 老铁送来的 LLVM 的 coroutine, LLVM 的 Coroutine 插件通过转换代码、生成状态机以及对协程切换点进行优化,以实现高效的协程执行。它针对协程切换点的位置进行标记,并利用生成的状态机来控制协程的切换。这样可以避免不必要的上下文切换,减少性能开销和内存占用。

总结#

Go 偷懒了,技术迭代了,Go 好像又赢了

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。