没有不值得去解决的问题,也没有不值得去学习的技术!

Go 语言并发实战:素数统计性能优化对比(“传统串行实现”与“多协程并发实现”)

在我的测试环境(2 核 CPU)下,统计 1-200,000 的素数,结果如下:如图1 - 传统串行实现:耗时约 25 秒。 - 多协程并发实现:耗时约 12 秒。

作者:

, ,

Go 并发编程实战步步通

其中一次执行结果输出了 hello,world 11 。小概率事件,或者说 执行结果大概率不会输出 “hello,world 12”及以后的输出。以下是一个简单的执行示意图1

(1) goroutine快速入门:多场景执行流程解析(附代码+流程图)

运行后,控制台要么打印空结果,要么只打印少量结果,大部分阶乘结果丢失。

(2) Go并发学习:从阶乘案例看Channel的必要性

测试代码(故意混用类型,验证报错) 测试结果(编译报错):如图1

(3) Go学习笔记:Channel基础用法与4种核心注意事项(附代码测试)

我运行了上述代码,整理出核心运行结果(截取部分),方便自己后续回顾:如图1

(4) Go复习笔记:Goroutine与Channel协同工作(解决加锁加休眠隐患)

Go 语言通道阻塞深度解析:从 Deadlock 到异步处理

(5) Go 语言通道阻塞深度解析:从 Deadlock 到异步处理

在我的测试环境(2 核 CPU)下,统计 1-200,000 的素数,结果如下:如图1 - 传统串行实现:耗时约 25 秒。 - 多协程并发实现:耗时约 12 秒。

(6) Go 语言并发实战:素数统计性能优化对比(“传统串行实现”与“多协程并发实现”)

运行结果:如图1

(7) Go 语言复习:使用 select 解决 Channel 读取阻塞问题

运行结果:如图1

(8) Go 语言中 Goroutine 的 Panic 捕获与 recover 机制

在编程学习中,判断素数是一个经典的算法问题。当数据量较小时,传统的串行循环足以应付;但当数据量扩大到几十万甚至更多时,如何提升计算效率就成了关键。

本文将基于 Go 语言,对比“传统串行实现”与“多协程并发实现”在统计 1-200,000 范围内素数时的性能差异,并深入分析背后的原理。

一、 需求与分析思路

任务目标:
统计 1 到 200,000 之间所有的素数,并对比不同实现方式的耗时。

1. 传统方法(串行):
使用一个 for 循环,从 1 遍历到 200,000。对每一个数字调用 isPrime 函数进行判断。这种方法所有计算都在主线程中按顺序执行,无法利用多核 CPU 的优势。

2. 并发方法(并行 – 任务队列模式):
核心思想是“动态负载均衡”。我们不再给每个协程分配固定的数字区间,而是建立一个“任务通道”。

架构设计:
– 任务生产者:启动一个协程,负责将 1 到 200,000 的数字依次放入 intChan(任务通道)。
– 任务消费者(多个 Goroutine):启动 2 个(或根据 CPU 核心数调整)Goroutine。这些协程不关心自己处理哪一段数字,它们只负责从 intChan 中“抢”任务。谁空闲,谁就从通道里取下一个数字进行判断。
– 结果收集(primeChan):如果某个协程判断当前数字是素数,就将其写入 primeChan。
– 同步控制(exitChan):当所有计算协程处理完通道中的所有任务后,通知主线程统计结束。

这种模式的好处是:如果某个数字判断特别耗时,不会阻塞其他协程,其他协程会继续从通道领取新任务,实现了真正的动态负载平衡。

二、 代码实现对比

1. 传统串行实现 (goroutine-apply02/main.go)

这种实现方式非常直观:

Go
package main

import (
	"fmt"
	"time"
)

func main() {

	start := time.Now().Unix()
	for num := 1; num <= 200000; num++ {

		flag := true //假设是素数
		//判断num是不是素数
		for i := 2; i < num; i++ {
			if num%i == 0 { //说明该num不是素数
				flag = false
				break
			}
		}

		if flag {
			//将这个数就放入到primeChan
			//primeChan<- num
			//将结果输出
			//fmt.Printf("素数=%d\n", num)
		}

	}
	end := time.Now().Unix()
	fmt.Println("普通的方法耗时=", end-start)

}

2. 多协程并发实现 (goroutine-apply/main.go)

并发实现利用了 Channel 进行任务分发:

Go
package main

import (
	"fmt"
	"time"
)

// 向 intChan 放入 1-200000 个数
func putNum(intChan chan int) {

	for i := 1; i <= 200000; i++ {
		intChan <- i
	}

	//关闭intChan
	close(intChan)
}

// 从 intChan取出数据,并判断是否为素数,如果是,就放入到 primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {

	// 使用 for 循环
	// var num int
	var flag bool //
	for {
		//time.Sleep(time.Millisecond * 10)
		num, ok := <-intChan //intChan 取不到..

		if !ok {
			break
		}
		flag = true //假设是素数
		//判断num是不是素数
		for i := 2; i < num; i++ {
			if num%i == 0 { //说明该num不是素数
				flag = false
				break
			}
		}

		if flag {
			//将这个数就放入到primeChan
			primeChan <- num
		}
	}

	fmt.Println("有一个primeNum 协程因为取不到数据,退出")
	//这里我们还不能关闭 primeChan
	//向 exitChan 写入true
	exitChan <- true

}

func main() {

	intChan := make(chan int, 1000)
	primeChan := make(chan int, 20000) //放入结果
	//标识退出的管道
	exitChan := make(chan bool, 2) // 2个

	start := time.Now().Unix()

	//开启一个协程,向 intChan 放入 1-200000 个数
	go putNum(intChan)
	//开启4个协程,从 intChan 取出数据,并判断是否为素数,如果是,就
	//放入到 primeChan
	for i := 0; i < 2; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}

	//这里我们主线程,进行处理
	//直接
	go func() {
		for i := 0; i < 2; i++ {
			<-exitChan
		}

		end := time.Now().Unix()
		fmt.Println("使用协程耗时=", end-start)

		//当我们从 exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
		close(primeChan)
	}()

	//遍历我们的 primeChan ,把结果取出
	for {
		_, ok := <-primeChan
		if !ok {
			break
		}
		//将结果输出
		//fmt.Printf("素数=%d\n", res)
	}

	fmt.Println("main线程退出")

}

三、 性能测试结果

在我的测试环境(2 核 CPU)下,统计 1-200,000 的素数,结果如下:如图1

在我的测试环境(2 核 CPU)下,统计 1-200,000 的素数,结果如下:如图1

- 传统串行实现:耗时约 25 秒。
 - 多协程并发实现:耗时约 12 秒。

– 传统串行实现:耗时约 25 秒。
– 多协程并发实现:耗时约 12 秒。

可以看到,并发实现的效率提升了 50% 左右。理论上来说,如果是 4 核 CPU,预计耗时大约为 6 秒左右。

四、 深度解析:为什么并发更快?

1. 动态任务分发:

正如你所指出的,primeNum 相关的协程是同时进行的。它们通过 for num := range intChan 共同竞争任务。这意味着,如果 CPU 有两个核心,那么同一时刻有两个协程在并行计算。当一个协程算完一个数字,它会立即去通道里拿下一个,不需要等待特定的“区间”结束。

2. CPU 核心利用率:

在串行模式下,只有一个核心在全速运转。而在并发模式下,Go 运行时调度器会将这 2 个 Goroutine 映射到可用的操作系统线程上,从而让两个核心同时参与素数判断的计算,显著减少了总等待时间。

3. 通道(Channel)的同步作用:

intChan 在这里扮演了“任务池”的角色。它解耦了任务的产生和消费。无论生产者是快是慢,消费者都能以最快的速度处理手头可用的任务。而 exitChan 则确保了主线程只有在所有子任务真正完成后才进行统计,避免了数据遗漏。

五、 总结与建议

通过这个素数统计的案例,我们可以得出以下结论:

1. 并发适合 CPU 密集型任务的拆分:对于大量的独立计算任务,利用多核优势可以显著缩短总耗时。
2. “任务队列”比“静态分片”更灵活:通过 Channel 分发任务,可以自动处理负载不均的问题,代码扩展性更好。
3. 通道是并发安全的基石:合理使用 Channel 可以避免复杂的锁操作,让并发代码更加简洁、易读。

在实际开发中,如果遇到类似的大规模数据处理场景,不妨尝试一下 Go 的并发模型,它往往能给你带来意想不到的性能惊喜。

Go 语言通道阻塞深度解析:从 Deadlock 到异步处理 Go 语言复习:使用 select 解决 Channel 读取阻塞问题

Go / Gin 后端接口维护与远程技术支持

如果你的 Go / Gin 后端接口、小型服务或内部系统需要维护、Bug 修复、接口排查、性能优化或小功能迭代,可以联系我。

适合以下情况:
✅ Go / Gin 接口报错或响应变慢
✅ 已有 Go 服务需要长期维护
✅ 后端 API 需要小功能迭代
✅ 定时任务、数据处理服务异常
✅ MySQL / Redis / Nginx 相关问题排查
✅ 需要远程兼职技术支持

可先从一次小问题开始:
✅ 接口异常排查
✅ 日志分析
✅ 性能瓶颈定位
✅ 部署环境检查
✅ 小功能评估与迭代

如需咨询,请联系我,并注明:Go / Gin 维护咨询

联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com