Go相比于老牌的Java,最大的特点无疑是原生支持高并发,而高并发支持的基点则来自于协程。
线程与协程
以Java为例,Java原生只有多线程的支持而没有协程(在Java19中引入了虚拟线程的概念),因此Java的并发能力完全依赖于线程。其每建立一个线程就需要付出约1MB左右的内存,对于资源昂贵的服务器来说线程无疑是一种昂贵的资源。
与此相对的,Go中的协程则非常的轻量,每一个协程只需要约几十KB的内存即可提供完整的服务。因此同时开启数十万个协程是可以实现的,而这对于线程来说则是不可想象的。
传统线程的并发模式
在传统的并发模式中,程序对于线程的应用必须小心翼翼,利用线程池小心的规划资源的使用,并且要时常注意对共享资源加锁以保证数据的合法性。同时在线程之间进行切换也有着相当高昂的成本。
协程并发
对于协程来说,抛弃掉了线程池管理的成本,协程能够自动调度。同时协程之间的通信更加简单,只需通过channel进行通信即可,避免了锁竞争。协程的资源占用更少且切换效率更高。 以分别计算1000个数求和再求综合的为例
class SumThread implements Runnable {
int[] array;
int start;
int end;
Result result;
SumThread(int[] array, int start, int end, Result result) {
this.array = array;
this.start = start;
this.end = end;
this.result = result;
}
public void run() {
int sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
result.addResult(sum);
}
}
class Result {
int total = 0;
public synchronized void addResult(int sum) {
total += sum;
}
}
public class Main {
public static void main(String[] args) {
int[] array = {1, 2, 3, ..., 1000};
Result result = new Result();
List<Thread> threads = new ArrayList<>();
int numThreads = 10;
int segment = array.length / numThreads;
for (int i = 0; i < numThreads; i++) {
int start = i * segment;
int end = (i+1) * segment;
Thread t = new Thread(new SumThread(array, start, end, result));
threads.add(t);
t.start();
}
for (Thread t : threads) {
t.join();
}
System.out.println(result.total);
}
}
package main
import (
"fmt"
"sync"
)
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将结果发送到通道
}
func main() {
list := []int{1, 2, 3, ..., 1000} // 1000个数字
c := make(chan int, 10)
var wg sync.WaitGroup
for i:= 0; i< 10; i++ {
wg.Add(1)
go sum(list[i*100:(i+1)*100], c) //启动10个goroutine并发求和
}
go func() {
wg.Wait()
close(c)
}()
var total int
for i := range c {
total += i
}
fmt.Println(total) // 结果
}
两段代码的差距显而易见,go实现起来比Java更简单且结构更加清晰,并且还不用处理锁的问题。