增加吞吐量 异步方式
17 | 异步RPC:压榨单机吞吐量 https://time.geekbang.org/column/article/216803
这并不是一个新话题,比如现在我们经常提到的响应式开发,就是为了能够提升业务处理的吞吐量。要提升吞吐量,其实关键就两个字:“异步”。我们的 RPC 框架要做到完全异步化,实现全异步 RPC。试想一下,如果我们每次发送一个异步请求,发送请求过后请求即刻就结束了,之后业务逻辑全部异步执行,结果异步通知,这样可以增加多么可观的吞吐量?
说到异步,我们最常用的方式就是返回 Future 对象的 Future 方式,或者入参为 Callback 对象的回调方式,而 Future 方式可以说是最简单的一种异步方式了。我们发起一次异步请求并且从请求上下文中拿到一个 Future,之后我们就可以调用 Future 的 get 方法获取结果。
连续发送 4 次异步请求并且拿到 4 个 Future,由于是异步调用,这段时间的耗时几乎可以忽略不计,之后我们统一调用这几个 Future 的 get 方法。这样一来的话,业务逻辑执行完的时间在理想的情况下是多少毫秒呢?没错,10 毫秒,耗时整整缩短到了原来的四分之一,也就是说,我们的吞吐量有可能提升 4 倍!
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func NOW() time.Time {
return time.Now()
}
func main() {
typeSync()
typeSync2()
typeAsync()
}
func typeSync() {
now := time.Now()
fmt.Println("main-in-typeSync", NOW())
// A slice of sample websites
urls := []string{
"https://www.easyjet.com/",
"https://www.skyscanner.de/",
"https://www.ryanair.com",
"https://wizzair.com/",
"https://www.swiss.com/",
}
for _, url := range urls {
checkUrl(url)
}
fmt.Println("main-out-typeSync", NOW(), time.Since(now))
}
func typeSync2() {
now := time.Now()
fmt.Println("main-in-typeSync-2", NOW())
// A slice of sample websites
urls := []string{
"https://www.easyjet.com/",
"https://www.skyscanner.de/",
"https://www.ryanair.com",
"https://wizzair.com/",
"https://www.swiss.com/",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
func(url string) {
checkUrl(url)
wg.Done()
}(url)
}
wg.Wait()
fmt.Println("main-out-typeSync-2", NOW(), time.Since(now))
}
func typeAsync() {
now := time.Now()
fmt.Println("main-in-typeAsync", NOW())
// A slice of sample websites
urls := []string{
"https://www.easyjet.com/",
"https://www.skyscanner.de/",
"https://www.ryanair.com",
"https://wizzair.com/",
"https://www.swiss.com/",
}
Len := len(urls)
ch := make(chan struct{}, Len)
for _, url := range urls {
go checkUrl2(url, ch)
}
for i := 0; i < Len; i++ {
<-ch
}
fmt.Println("main-out-typeAsync", NOW(), time.Since(now))
}
//checks and prints a message if a website is up or down
func checkUrl(url string) {
fmt.Println("checkUrl-in", NOW())
_, err := http.Get(url)
if err != nil {
fmt.Println(url, "is down !!!")
return
}
fmt.Println(url, "is up and running.")
}
func checkUrl2(url string, ch chan struct{}) {
fmt.Println("checkUrl-in", NOW())
_, err := http.Get(url)
if err != nil {
fmt.Println(url, "is down !!!")
} else {
fmt.Println(url, "is up and running.")
}
ch <- struct{}{}
}
main-in-typeSync 2021-07-19 10:36:50.121365413 +0800 CST m=+0.000262889
checkUrl-in 2021-07-19 10:36:50.12154002 +0800 CST m=+0.000437775
https://www.easyjet.com/ is up and running.
checkUrl-in 2021-07-19 10:36:51.018703401 +0800 CST m=+0.897600877
https://www.skyscanner.de/ is up and running.
checkUrl-in 2021-07-19 10:36:51.422785782 +0800 CST m=+1.301683258
https://www.ryanair.com is up and running.
checkUrl-in 2021-07-19 10:36:53.203393402 +0800 CST m=+3.082291437
https://wizzair.com/ is up and running.
checkUrl-in 2021-07-19 10:36:53.898538933 +0800 CST m=+3.777436409
https://www.swiss.com/ is up and running.
main-out-typeSync 2021-07-19 10:36:54.009832027 +0800 CST m=+3.888729503 3.888471084s
main-in-typeSync-2 2021-07-19 10:36:54.010094635 +0800 CST m=+3.888992111
checkUrl-in 2021-07-19 10:36:54.010245216 +0800 CST m=+3.889142692
https://www.easyjet.com/ is up and running.
checkUrl-in 2021-07-19 10:36:54.716210684 +0800 CST m=+4.595108160
https://www.skyscanner.de/ is up and running.
checkUrl-in 2021-07-19 10:36:54.911257626 +0800 CST m=+4.790155102
https://www.ryanair.com is up and running.
checkUrl-in 2021-07-19 10:36:56.179238043 +0800 CST m=+6.058135799
https://wizzair.com/ is up and running.
checkUrl-in 2021-07-19 10:36:56.373953049 +0800 CST m=+6.252850525
https://www.swiss.com/ is up and running.
main-out-typeSync-2 2021-07-19 10:36:56.410410604 +0800 CST m=+6.289308359 2.400320439s
main-in-typeAsync 2021-07-19 10:36:56.410705898 +0800 CST m=+6.289603374
checkUrl-in 2021-07-19 10:36:56.410869889 +0800 CST m=+6.289767365
checkUrl-in 2021-07-19 10:36:56.411164345 +0800 CST m=+6.290085847
checkUrl-in 2021-07-19 10:36:56.411742362 +0800 CST m=+6.290639838
checkUrl-in 2021-07-19 10:36:56.411882327 +0800 CST m=+6.290779803
checkUrl-in 2021-07-19 10:36:56.412122027 +0800 CST m=+6.291019503
https://www.swiss.com/ is up and running.
https://www.skyscanner.de/ is up and running.
https://wizzair.com/ is up and running.
https://www.easyjet.com/ is up and running.
https://www.ryanair.com is up and running.
main-out-typeAsync 2021-07-19 10:36:57.703618233 +0800 CST m=+7.582515709 1.292916805s
https://medium.com/@gauravsingharoy/asynchronous-programming-with-go-546b96cd50c1
package main
import (
"fmt"
"time"
)
func DoneAsync() chan struct{} {
r := make(chan struct{})
fmt.Println("Warming up ...")
go func() {
time.Sleep(3 * time.Second)
r <- struct{}{}
fmt.Println("Done ...")
}()
return r
}
func main() {
fmt.Println("Let's start ...")
val := DoneAsync()
fmt.Println("Done is running ...")
fmt.Println(<-val)
}
Async/Await in Golang: An Introductory Guide | Hacker Noon https://hackernoon.com/asyncawait-in-golang-an-introductory-guide-ol1e34sg
Golang is a concurrent programming language. It has powerful features like Goroutines and Channels that can handle asynchronous tasks very well. Also, goroutines are not OS threads, and that's why you can spin up as many goroutines as you want without much overhead, it's stack size starts at 2KB only. So why async/await? Async/Await is a nice language feature that provides a simpler interface to asynchronous programming.
Project Link: https://github.com/Joker666/AsyncGoDemo
How Does it Work?
Started with F# and then C#, now in Python and Javascript, async/await is an extremely popular feature of a language. It simplifies the asynchronous method execution structure and, it reads like synchronous code. So much easier to follow for developers. Let's see a simple example in C# how async/await works
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;
}
We have the Main function that would be executed when the program is run. We have DoneAsync which is an async function. We stop the execution of the code with Delay function for 3 seconds. Delay is an async function itself, so we call it with await.
await only blocks the code execution within the async function
In the main function, we do not call DoneAsync with await. But the execution starts for DoneAsync. Only when we await it, we get the result back. The execution flow looks like this
Let's start ...
Warming up ...
Done is running ...
Done ...
1
This looks incredibly simple for asynchronous execution. Let's see how we can do it with Golang using Goroutines and 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)
}
Here, DoneAsync runs asynchronously and returns a channel. It writes a value to the channel once it's done executing the async task. In main function, we invoke DoneAsync and keep doing our operations and then we read the value from the returned channel. It is a blocking call that waits till the value is written to the channel and after it gets the value it writes to the console.
Let's start ...
Warming up ...
Done is running ...
Done ...
1
We see, we achieve the same outcome as the C# program but it doesn't look as elegant as async/await. While this is actually good, we are able to do a lot more granular things with this approach much easily, we can also implement async/await keywords in Golang with a simple struct and interface. Let's try that.
Implementing Async/Await
The full code is available in the project link. To implement async/await in Golang, we will start with a package directory named async. The project structure looks like
.
├── async
│ └── async.go
├── main.go
└── README.md
In the async file, we write the simplest future interface that can handle async tasks.
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
}
},
}
}
Not a lot is happening here, we add a Future interface that has the Await method signature. Next, we add a future struct that holds one value, a function signature of the await function. Now futute struct implements Future interface's Await method by invoking its own await function.
Next in the Exec function, we execute the passed function asynchronously in goroutine. And we return the await function. It waits for the channel to close or context to read from. Based on whichever happens first, it either returns the error or the result which is an interface.
Now armed with this new async package, let's see how we can change our current go code
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)
}
At the first glance, it looks much cleaner, we are not explicitly working with goroutine or channels here. Our DoneAsync function has been changed to a completely synchronous nature. In the main function, we use the async package's Exec method to handle DoneAsync. Which starts the execution of DoneAsync. The control flow is returned back to main function which can execute other pieces of code. Finally, we make blocking call to Await and read back data.
Now the code looks much simpler and easier to read. We can modify our async package to incorporate a lot of other types of asynchronous tasks in Golang, but we would just stick to simple implementation for now in this tutorial.
Conclusion
We have gone through what async/await it and implemented a simple version of that in Golang. I would encourage you to look into async/await a lot more and see how it can ease the readability of the codebase much better.
package main
import (
"context"
"fmt"
"time"
)
type Future interface {
Await() interface{}
}
type future struct {
await func(ctx context.Context) interface{}
}
func (f future) Await() interface{} {
return f.await(context.Background())
}
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
}
},
}
}
func DoneAsync() struct{} {
fmt.Println("Warming up ...")
time.Sleep(3 * time.Second)
fmt.Println("Done ...")
return struct{}{}
}
func main() {
fmt.Println("Let's start ...")
future := Exec(func() interface{} {
return DoneAsync()
})
fmt.Println("Done is running ...")
val := future.Await()
fmt.Println(val)
}
What I've learned today - 5 - Using Golang channels as async await - DEV Community https://dev.to/lgdev07/what-i-ve-learned-today-5-using-golang-channels-as-async-await-a0i
For people accustomed to using Javascript, they have certainly used or heard about async / await, it is a feature of the language that allows working with asynchronous programming.
Golang has channels and goroutines, allowing this to be used very smoothly, however, people who have just come to the language may not know how to do it, to have a base, I prepared an example that can help to understand.
We will use the public pokeapi api.
How it is done in Javascript
const awaitTask = async () => {
let response = await fetch('https://pokeapi.co/api/v2/pokemon/ditto');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response
}
awaitTask()
.then(r => r.json())
.then(r => console.log(r))
How we do it in Golang
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func awaitTask() <-chan string {
fmt.Println("Starting Task...")
c := make(chan string)
go func() {
resp, err := http.Get("https://pokeapi.co/api/v2/pokemon/ditto")
if err != nil {
log.Fatalln(err)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
}
c <- string(body)
fmt.Println("...Done!")
}()
return c
}
func main() {
value := <-awaitTask()
fmt.Println(value)
}
How to make:
- Set the return type to be a channel.
- Create a channel to return a value.
- Add a go func for asynchronous execution.
- Within the function, assign the value to the channel.
- At the end of the function, indicate the return of the channel with the value.
- In the main function, assign the return of the channel to a variable.