Go 语言并发编程的 “倚天剑” 和 “屠龙刀”

0 阅读2分钟

Go 语言并发编程的 “倚天剑” 和 “屠龙刀”

goroutine(协程)和 channel(通道);goroutine类似于ts中await的异步效果,channel是goroutine中通信工具。

不要通过共享内存来通信,而要通过通信来共享内存。

具体内容请查看并发 | Golang 中文学习文档

使用场景

「同时干多件事,不用傻等」

痛点:比如你要调用 3 个不同的微服务接口获取数据,如果串行调用,每个耗时 1s,总共需要 3s。

解决:用 goroutine 让它们同时跑,总共只需要 1s。

package main

import (
	"fmt"
	"sync"
	"time"
)

func fetchAPI(name string, wg *sync.WaitGroup) {
	defer wg.Done() // 本 goroutine 完成后计数 -1

	fmt.Println("开始调用:", name)
	time.Sleep(1 * time.Second) // 模拟网络耗时
	fmt.Println("调用完成:", name)
}

func main() {
	start := time.Now()

	var wg sync.WaitGroup // WaitGroup 解决主进程执行太快导致,子协程没执行完的问题
	wg.Add(3)             // 需要等待 3 个 goroutine

	go fetchAPI("用户服务", &wg)
	go fetchAPI("订单服务", &wg)
	go fetchAPI("库存服务", &wg)
	/*
	* 开始调用: 用户服务
	* 开始调用: 订单服务
	* 开始调用: 库存服务
	* 调用完成: 库存服务
	* 调用完成: 用户服务
	* 调用完成: 订单服务
	 */

	wg.Wait() // 用于等待子协程结束,否则就阻塞

	fmt.Printf("总共耗时: %v\n", time.Since(start))
}

「生产者 - 消费者模式」

痛点:一个模块负责生产数据(比如读文件),另一个模块负责处理数据(比如解析文件)。如果紧耦合在一起,代码很难维护。

解决:用 channel 当 “队列”,生产者只管往里塞,消费者只管往外取。

package main

import "fmt"

// 生产者:只管发送数据到 channel
func writch(ch chan<- int) {
	for i := 1; i <= 5; i++ {
		fmt.Println("生产了:", i)
		ch <- i
	}
	close(ch)
}

// 消费者:只管从 channel 接收数据
func readch(ch <-chan int) {
	for num := range ch {
		fmt.Println("消费了:", num)
	}
}

func main() {
	ch := make(chan int, 3) // 带缓冲的通道,作为仓库

	go writch(ch)
	readch(ch)
	/*
	生产了: 1
	生产了: 2
	生产了: 3
	生产了: 4
	生产了: 5
	消费了: 1
	消费了: 2
	消费了: 3
	消费了: 4
	消费了: 5
	*/
}

「限流控制:同时只能有 N 个人进房间」

痛点:虽然 goroutine 很轻,但如果同时开 10 万个 goroutine 去查数据库,数据库会直接崩掉。

解决:用带缓冲的 channel 当 “令牌桶”,限制并发数量。

package main

import (
	"fmt"
	"time"
)

func main() {
	// 缓冲大小为 3,意味着同时最多只能有 3 个 goroutine 在执行
	limitCh := make(chan bool, 3)

	for i := 1; i <= 10; i++ {
		go func(id int) {
			limitCh <- true // 尝试获取令牌(如果满了,就阻塞在这)
			
			fmt.Printf("任务 %d 开始执行\n", id)
			time.Sleep(1 * time.Second) // 模拟耗时操作
			fmt.Printf("任务 %d 执行完毕\n", id)

			<-limitCh // 释放令牌
		}(i)
	}

	// 简单等待所有任务完成
	time.Sleep(4 * time.Second)
  //3 个一组分批执行的,这就是限流。
}