惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
Cloudbric
Cloudbric
C
CERT Recently Published Vulnerability Notes
G
GRAHAM CLULEY
V
Vulnerabilities – Threatpost
C
Cisco Blogs
T
Tenable Blog
P
Privacy International News Feed
T
The Exploit Database - CXSecurity.com
I
Intezer
AWS News Blog
AWS News Blog
IT之家
IT之家
博客园 - 司徒正美
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 【当耐特】
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Spread Privacy
Spread Privacy
S
SegmentFault 最新的问题
博客园 - Franky
人人都是产品经理
人人都是产品经理
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
V
Visual Studio Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
H
Hacker News: Front Page
Latest news
Latest news
Scott Helme
Scott Helme
腾讯CDC
宝玉的分享
宝玉的分享
大猫的无限游戏
大猫的无限游戏
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
A
Arctic Wolf
S
Securelist
雷峰网
雷峰网
The GitHub Blog
The GitHub Blog
Project Zero
Project Zero
Google DeepMind News
Google DeepMind News
P
Palo Alto Networks Blog
F
Fortinet All Blogs
Schneier on Security
Schneier on Security
云风的 BLOG
云风的 BLOG
Security Archives - TechRepublic
Security Archives - TechRepublic
The Last Watchdog
The Last Watchdog
WordPress大学
WordPress大学
MongoDB | Blog
MongoDB | Blog
L
LINUX DO - 最新话题
S
Schneier on Security
NISL@THU
NISL@THU
Jina AI
Jina AI
M
MIT News - Artificial intelligence

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
goroutine 交替打印奇偶数
2023-04-20 · via 蛮荆

2023-04-20 Golang 并发编程 笔试

题目

使用两个 goroutine 交替并且顺序地打印数字 1…10, 要求其中一个 goroutine 打印奇数,另一个 goroutine 打印偶数。

交替打印奇偶数

本文尝试通过 Go 标准库提供的 API 来给出几种解决方案,虽然这看上去有点儿 “八股文” 的味道,但是对于提高 goroutine + channel 编码技巧还是非常有帮助的。

channel 方法

我们可以直接使用 goroutine + channel 的方式,写出直观的代码:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	// 打印奇数
	go func() {
		for n := range ch1 {
			fmt.Printf("channel-1 print %d\n", n)
			// 奇数 + 1 等于下一个要打印的偶数
			// 发送到 channel 2
			ch2 <- n + 1

			if n == 9 {
				break
			}
		}
		close(ch1)
	}()

	// 打印偶数
	go func() {
		for n := range ch2 {
			fmt.Printf("channel-2 print %d\n", n)
			if n == 10 {
				break
			}

			// 偶数 + 1 等于下一个要打印的奇数
			// 发送到 channel 1
			ch1 <- n + 1
		}
		close(ch2)
	}()

	ch1 <- 1

	// 等待 goroutine 执行完成
	time.Sleep(time.Second)
}

运行代码打印如下:

channel-1 print 1
channel-2 print 2
channel-1 print 3
channel-2 print 4
channel-1 print 5
channel-2 print 6
channel-1 print 7
channel-2 print 8
channel-1 print 9
channel-2 print 10

channel 优化版本

上面的代码虽然可以正确运行,但是存在两个小问题:

  1. 两个 goroutine 代码块堆叠在一起,有些冗余和重复,可以重构为一个函数
  2. 使用 time.Sleep 方法等待 goroutine 执行完成,不够优雅且存在很多潜在问题 (例如真实场景下我们得知 goroutine 的具体运行时间),可以使用 sync.WaitGroup 同步原语进行重构

优化过的代码如下:

package main

import (
	"fmt"
	"sync"
)

// rev  接收 channel
// send 发送 channel
// no   channel 编号 (提高打印消息的可读性)
// last channel 接收的最后一个数字, 超过该数字时结束打印
func printChar(rev <-chan int, send chan<- int, no, last int) {
	for i := range rev {
		if i > last {
			break
		}
		fmt.Printf("channel-%d print %d\n", no, i)
		send <- i + 1
	}
	close(send)
}

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	var wg sync.WaitGroup
	wg.Add(2)

	// 打印奇数
	go func() {
		defer wg.Done()
		printChar(ch1, ch2, 1, 9)
	}()

	// 打印偶数
	go func() {
		defer wg.Done()
		printChar(ch2, ch1, 2, 10)
	}()

	ch1 <- 1
	wg.Wait()
}

通过将打印过程封装为一个函数,然后交替传递 channel 参数,再使用 sync.WaitGroup 同步原语控制打印何时结束,避免了 time.Sleep 方法这种有明显缺陷的方案。

运行代码打印如下:

channel-1 print 1
channel-2 print 2
channel-1 print 3
channel-2 print 4
channel-1 print 5
channel-2 print 6
channel-1 print 7
channel-2 print 8
channel-1 print 9
channel-2 print 10

sync.Mutex 互斥锁

除了最基础的 channel 之外, 还可以使用标准库提供的同步原语来解决这个问题。

  • 当打印奇数的 goroutine 获取到锁时,打印奇数并切换下一次打印的 goroutine 编号,最后通过解锁操作来唤醒偶数 goroutine
  • 当打印偶数的 goroutine 获取到锁时,打印偶数并切换下一次打印的 goroutine 编号,最后通过解锁操作来唤醒奇数 goroutine
package main

import (
	"fmt"
	"sync"
)

// cond sync.Mutex 互斥锁
// cur  指定当前执行的 goroutine 编号
// no   channel 编号 (提高打印消息的可读性)
// last channel 接收的最后一个数字, 超过该数字时结束打印
func printChar(mu *sync.Mutex, cur *int, no, last int) {
	for i := no; i <= last; {
		mu.Lock()
		if *cur != no {
			// 如果当前执行 goroutine 编号不是自身
			mu.Unlock()
			continue
		}

		fmt.Printf("channel-%d print %d\n", no, i)

		// 指定当前执行的 goroutine 编号为另外一个 goroutine
		*cur = 3 - *cur
		i = i + 2
		// 唤醒另外一个 goroutine
		mu.Unlock()
	}
}

func main() {
	mu := &sync.Mutex{}

	// 当前执行的 goroutine 编号
	cur := 1

	var wg sync.WaitGroup
	wg.Add(2)

	// 打印奇数
	go func() {
		defer wg.Done()
		printChar(mu, &cur, 1, 9)
	}()

	// 打印偶数
	go func() {
		defer wg.Done()
		printChar(mu, &cur, 2, 10)
	}()

	wg.Wait()
}

sync.Cond 条件变量

在互斥锁的代码基础上稍加改造,即可适配 sync.Cond 条件变量同步原语代码。

package main

import (
	"fmt"
	"sync"
)

// cond sync.Cond 条件变量
// cur  指定当前执行的 goroutine 编号
// no   channel 编号 (提高打印消息的可读性)
// last channel 接收的最后一个数字, 超过该数字时结束打印
func printChar(cond *sync.Cond, cur *int, no, last int) {
	for i := no; i <= last; i = i + 2 {
		cond.L.Lock()
		for *cur != no {
			// 如果当前执行 goroutine 编号不是自身
			// 等待被唤醒
			cond.Wait()
		}

		fmt.Printf("channel-%d print %d\n", no, i)

		// 指定当前执行的 goroutine 编号为另外一个 goroutine
		*cur = 3 - *cur
		// 唤醒另外一个 goroutine
		cond.Signal()
		cond.L.Unlock()
	}
}

func main() {
	cond := sync.NewCond(&sync.Mutex{})

	// 当前执行的 goroutine 编号
	cur := 1

	var wg sync.WaitGroup
	wg.Add(2)

	// 打印奇数
	go func() {
		defer wg.Done()
		printChar(cond, &cur, 1, 9)
	}()

	// 打印偶数
	go func() {
		defer wg.Done()
		printChar(cond, &cur, 2, 10)
	}()

	wg.Wait()
}

小结

本文通过一个笔试题,给出了 Go 中 goroutine + channel 组合使用时常见的同步解决方案,希望能帮助读者在扩展 goroutine + channel 的基础应用同时,深入理解各同步原语之间的差异及其使用场景。