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

推荐订阅源

T
Tailwind CSS Blog
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Y
Y Combinator Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 聂微东
L
LangChain Blog
博客园_首页
Recent Announcements
Recent Announcements
月光博客
月光博客
酷 壳 – CoolShell
酷 壳 – CoolShell
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hackread – Cybersecurity News, Data Breaches, AI and More
爱范儿
爱范儿
博客园 - 叶小钗
博客园 - 【当耐特】
The Cloudflare Blog
J
Java Code Geeks
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
aimingoo的专栏
aimingoo的专栏
A
About on SuperTechFans

蛮荆

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