从炒菜到编程:揭秘Java/Go/Rust异步背后的哲学
要想开发出一款高效的程序,异步编程是必不可少的,而且Java、Go Rust 语言都通过不同方式支持异步编程。
何为异步
那么何为异步编程呢?
与异步相对的概念是同步,同步是指做完一件事再继续做另一件事,依次按照顺序完成每一件事。而异步恰恰相反,多件事情可以并行做,或者交替执行。举一个相对生活化的例子,来比较形象的说明下同步和异步的区别。
比如今天晚饭想给自己加个菜,于是开始按照菜谱进行炒菜,首先向锅中倒入食用油,等油热之后再放入相应的食材和调味品,然后进行翻炒,最后放入盘中进行享用。
现在可以吃饭了,现在可以一口米饭一口菜的干饭了,不过这时候还想看个视频或者听个音乐,一边干饭一边娱乐下。
这个例子中炒菜的过程就是同步的,必须把某一步做完之后再进行下一步。而吃饭的过程就是异步的,吃米饭和菜可以交替执行,吃饭和娱乐可以并行执行。下面用一个图来更形象的感受下同步和异步。

接下来我们看下各个编程语言是如何进行异步编程的,这里主要看下Java、Golang和Rust的异步编程。
Java异步编程
Java是通过多线程来实现异步编程的,主线程会为每个任务新建一个任务线程,各个任务线程之间无需相互等待,而是并行执行,相互之间没有影响,所有任务线程都执行结束之后主线程运行结束。
public class AsyncDiner {
public static void main(String[] args) {
Thread eatRiceThread = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) {
System.out.println("吃口米饭");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread eatDishThread = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) {
System.out.println("吃口菜");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread entertainThread = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) {
System.out.println("娱乐中。。。");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 启动三个线程
eatRiceThread.start();
eatDishThread.start();
entertainThread.start();
// 等待所有线程完成
try {
eatRiceThread.join();
eatDishThread.join();
entertainThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("干饭+娱乐全部完成!");
}
}
Code language: JavaScript (javascript)
但是随着Java不断的发展,目前已支持多种方式来实现异步编程,比如CompletableFuture,还可以通过第三方来实现更高效的异步编程。
Golang异步编程
Golang是通过协程来实现异步编程的。Golang的协程是其语言的一大特性,其使用简单,只需在需要异步执行的地方加上go
关键字即可,而且性能也很高。
package main
import (
"fmt"
"sync"
"time"
)
func eatRice(wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 3; i++ {
fmt.Println("吃口米饭")
time.Sleep(1 * time.Second)
}
}
func eatDish(wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 3; i++ {
fmt.Println("吃口菜")
time.Sleep(1 * time.Second)
}
}
func entertain(wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 3; i++ {
fmt.Println("娱乐中。。。")
time.Sleep(1 * time.Second)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(3) // 启动三个并发任务
go eatRice(&wg)
go eatDish(&wg)
go entertain(&wg)
wg.Wait() // 等待所有 goroutine 结束
fmt.Println("干饭+娱乐全部完成!")
}
Code language: JavaScript (javascript)
Rust异步编程
Rust也是通过协程来实现异步编程的,通过async/await
语法实现,与Golang不同的是,Rust并没有自带异步运行时,而是靠社区提供,这里的例子使用tokio
提供异步运行时。
由于使用了第三方库,所以需要在项目的Cargo.toml
中添加相应的依赖,如下:
# [dependencies]
tokio = { version = "1.45.0", features = ["full"] }
Code language: PHP (php)
例子代码如下:
use tokio::time::{sleep, Duration};
async fn eat_rice() {
for i in 1..=3 {
println!("吃口米饭");
sleep(Duration::from_secs(1)).await;
}
}
async fn eat_dish() {
for i in 1..=3 {
println!("吃口菜");
sleep(Duration::from_secs(1)).await;
}
}
async fn entertain() {
for i in 1..=3 {
println!("娱乐中。。。");
sleep(Duration::from_secs(1)).await;
}
}
#[tokio::main]
async fn main() {
// 并发执行三个异步任务
let ((), (), ()) = tokio::join!(
eat_rice(),
eat_dish(),
entertain(),
);
println!("干饭+娱乐全部完成!");
}
Code language: PHP (php)
线程和协程
上文中提到两个概念,一个是线程,另一个是协程。
- 线程是操作系统中调度的最小单元,是进程的一个执行路径,由操作系统进行调度,是抢占式的。线程之间通过共享内存来通信。
- 协程是用户态的轻量级线程,由程序自己控制,不受操作系统调度,是协作式的。协程之间通过Channel进行通信。
线程是由CPU执行,在单核CPU上,多个线程轮流执行,这时线程的切换需要交换上下文,开销较大,在多核CPU上,多个线程可以并行执行,可以并行的线程数与核数相同,线程再多就需要排队等待CPU调度了。而协程是在线程中执行,单个线程中可并行多个协程,所以协程之间的切换不涉及上下文的交换,开销较小。
总结
这里通过炒菜和吃饭的例子介绍了什么是同步和异步,同时通过代码示例介绍了Java、Golang和Rust的异步实现,其中Java通过线程实现异步,而Golang和Rust则是通过协程来实现异步,其线程没有协程的性能高,但也并不是协程就一定比线程好,只是各自适合的场景不一样,其中线程比较适合CPU密集型的大任务,可以多核并行执行,比如一些大数据计算场景,而协程比较适合IO密集型的轻量任务,可以高并发快速执行,比如一些http请求。
这篇文章主要介绍下何为异步编程和各个编程语言都是如何实现的,在接下来的文章中会逐步介绍下Rust中async
的原理和async
在一些具体的场景中如何发挥其巨大的作用的。