写点什么

使用 Go 实现 Async/Await 模式

用户头像
Roc
关注
发布于: 2020 年 12 月 01 日
使用 Go 实现 Async/Await 模式

概述

Golang 是一种并发编程语言。它具有强大的特性,如 Goroutines 和 Channels,可以很好地处理异步任务。另外,goroutines 不是 OS 线程,这就是为什么您可以在不增加开销的情况下根据需要启动任意数量的 goroutine 的原因,它的堆栈大小初始化时仅 2KB。那么为什么要 async/await 呢?Async/Await 是一种很好的语言特点,它为异步编程提供了更简单的接口。


项目链接:https://github.com/icyxp/AsyncGoDemo

它是如何工作的?

从 F# 开始,然后是 C#,到现在 Python 和 Javascript 中,async/await 是一种非常流行的语言特点。它简化了异步方法的执行结构并且读起来像同步代码。对于开发人员来说更容易理解。让我们看看 c# 中的一个简单示例 async/await 是如何工作的。

static async Task Main(string[] args)
{
Console.WriteLine("Let's start ...");
var done = DoneAsync();
Console.WriteLine("Done is running ...");
Console.WriteLine(await done);
}
static async Task<int> DoneAsync()
{
Console.WriteLine("Warming up ...");
await Task.Delay(3000);
Console.WriteLine("Done ...");
return 1;
}
复制代码

当程序运行时,我们的 Main 函数将被执行。我们有异步函数 DoneAsync。我们使用 Delay 方法停止执行代码 3 秒钟。Delay 本身是一个异步函数,所以我们用 await 来调用它。


await 只阻塞异步函数内的代码执行


在 Main 函数中,我们不使用 await 来调用 DoneAsync。但 DoneAsync 开始执行后,只有当我们 await 它的时候,我们才会得到结果。执行流程如下所示:

Let's start ...Warming up ...Done is running ...Done ...1
复制代码

对于异步执行,这看起来非常简单。让我们看看如何使用 Golang 的 Goroutines 和 Channels 来做到这一点。

func DoneAsync() chan int {
r := make(chan int)
fmt.Println("Warming up ...")
go func() {
time.Sleep(3 * time.Second)
r <- 1
fmt.Println("Done ...")
}()
return r
}
func main () {
fmt.Println("Let's start ...")
val := DoneAsync()
fmt.Println("Done is running ...")
fmt.Println(<- val)
}
复制代码

在这里,DoneAsync 异步运行并返回一个 channel。执行完异步任务后,它会将值写入 channel。在 main 函数中,我们调用 DoneAsync 并继续执行后续操作,然后从返回的 channel 读取值。它是一个阻塞调用,等待直到将值写入 channel,并在获得值后将其输出到终端。

Let's start ...Warming up ...Done is running ...Done ...1
复制代码

我们看到,我们实现了与 C# 程序相同的结果,但它看起来不像 async/await 那样优雅。尽管这确实不错,但是我们可以使用这种方法轻松地完成很多细粒度的事情,我们还可以用一个简单的结构和接口在 Golang 中实现 async/await 关键字。让我们试试。

实现 Async/Await


完整代码可在项目链接中找到(在文章开始的地方)。要在 Golang 中实现 async/await,我们将从一个名为 async 的包目录开始。项目结构看起来是这样的。

.├── async│   └── async.go├── main.go└── README.md 
复制代码

在 async 文件中,我们编写了可以处理异步任务最简单的 Future 接口。

package async
import "context"
// Future interface has the method signature for await
type Future interface {
Await() interface{}
}
type future struct {
await func(ctx context.Context) interface{}
}
func (f future) Await() interface{} {
return f.await(context.Background())
}
// Exec executes the async function
func Exec(f func() interface{}) Future {
var result interface{}
c := make(chan struct{})
go func() {
defer close(c)
result = f()
}()
return future{
await: func(ctx context.Context) interface{} {
select {
case <-ctx.Done():
return ctx.Err()
case <-c:
return result
}
},
}
}
复制代码

这里发生的事情并不多,我们添加了一个具有 Await 方法标识的 Future 接口。接下来,我们添加一个 future 结构,它包含一个值,即 await 函数的函数标识。现在 futute struct 通过调用自己的 await 函数来实现 Future 接口的 Await 方法。


接下来在 Exec 函数中,我们在 goroutine 中异步执行传递的函数。然后返回 await 函数。它等待 channel 关闭或 context 读取。基于最先发生的情况,它要么返回错误,要么返回作为接口的结果。


现在,有了这个新的 async 包,让我们看看如何更改当前的 go 代码:

func DoneAsync() int {
fmt.Println("Warming up ...")
time.Sleep(3 * time.Second)
fmt.Println("Done ...")
return 1
}
func main() {
fmt.Println("Let's start ...")
future := async.Exec(func() interface{} {
return DoneAsync()
})
fmt.Println("Done is running ...")
val := future.Await()
fmt.Println(val)
}
复制代码

乍一看,它看起来干净得多,这里我们没有显式地使用 goroutine 或 channels。我们的 DoneAsync 函数已更改为完全同步的性质。在 main 函数中,我们使用 async 包的Exec 方法来处理 DoneAsync。在开始执行 DoneAsync。控制流返回到可以执行其他代码的 main 函数中。最后,我们对 Await 进行阻塞调用并回读数据。


现在,代码看起来更加简单易读。我们可以修改我们的 async 包从而能在 Golang 中合并许多其他类型的异步任务,但在本教程中,我们现在只坚持简单的实现。


结论

我们经历了 async/await 的过程,并在 Golang 中实现了一个简单的版本。我鼓励您进一步研究 async/await,看看它如何更好的让代码库便于易读。


译自:https://hackernoon.com/asyncawait-in-golang-an-introductory-guide-ol1e34sg



发布于: 2020 年 12 月 01 日阅读数: 1205
用户头像

Roc

关注

公众号:云原生领域 2020.10.20 加入

专注于云原生领域,对容器技术、K8S、ServiceMesh、Go技术持续追踪

评论 (1 条评论)

发布
用户头像
个人感觉意义不大。C#的async/await代码中的“异步阻塞点(比如Task.Wait)”实际上也不是真的阻塞线程,而是一个可以yield的点,通过yield让线程从当前task(也就是用户态线程)当中跳出来,调度下一个可运行的Task继续执行;而Go语言里就屏蔽了线程,只有goroutine,它的“阻塞”就是类似async/await中的异步阻塞一样,并不真正阻塞底层的线程,Go最大的优势就在于直接用同步代码,只要放在goroutine里运行,它的async/await是自动的。
2020 年 12 月 01 日 22:14
回复
没有更多了
使用 Go 实现 Async/Await 模式