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

推荐订阅源

PCI Perspectives
PCI Perspectives
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Palo Alto Networks Blog
S
Schneier on Security
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
K
Kaspersky official blog
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tenable Blog
G
GRAHAM CLULEY
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
I
Intezer
D
Docker
月光博客
月光博客
L
Lohrmann on Cybersecurity
Latest news
Latest news
B
Blog
罗磊的独立博客
M
MIT News - Artificial intelligence
S
Securelist
Know Your Adversary
Know Your Adversary
Help Net Security
Help Net Security
Recorded Future
Recorded Future
S
SegmentFault 最新的问题
N
Netflix TechBlog - Medium
T
Threatpost
H
Hacker News: Front Page
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
人人都是产品经理
人人都是产品经理
F
Fortinet All Blogs
博客园 - Franky
P
Proofpoint News Feed
大猫的无限游戏
大猫的无限游戏
Blog — PlanetScale
Blog — PlanetScale
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
A
About on SuperTechFans
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tor Project blog
Google Online Security Blog
Google Online Security Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Engineering at Meta
Engineering at Meta
Webroot Blog
Webroot Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
Microsoft Security Blog
Microsoft Security Blog

蛮荆

如何获取更多的免费服务器 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 的基础应用同时,深入理解各同步原语之间的差异及其使用场景。