使用 golang
的并发特性,可以很好的提高程序的执行速度。其中,并发版本的素数筛是一个经典的例子,通过它我们可以更深刻地理解Go语言的并发特性。
首先,我们看看什么是素数筛:
顾名思义,就是将素数筛选出来。首先,我们是怎么判断一个数是素数的呢?我们一般是这样做的:
// 判断整数 n 是否是素数
func isPrime(n int) bool {
for i := 2; i*i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}
之后,就是暴力循环喽。但是,显然,这样做不够高效。
素数筛算法不是这样。他的思想是这样的:
首先从 2 开始,我们知道 2 是一个素数,那么 2 × 2 = 4, 3 × 2 = 6, 4 × 2 = 8… 都不可能是素数了。
然后我们发现 3 也是素数,那么 3 × 2 = 6, 3 × 3 = 9, 3 × 4 = 12… 也都不可能是素数了。
根据这个算法,我们可以有:
// 返回生成[0,n] 之内的所有素数
func PrimeSieve(n int) []int {
// 标记数组,用于标记一个数是不是素数,是素数标记为0
mark := make([]int, n+1)
// 0,1 不是素数,从2开始
mark[0], mark[1] = 1, 1
for i := 2; i <= n; i++ {
if mark[i] == 0 {
for j := 2 * i; j <= n; j += i{
mark[j] = 1 // 标记为 非素数
}
}
}
result := make([]int, 0)
for i := 2; i <= n; i++ {
if mark[i] == 0 {
result = append(result, i)
}
}
return result
}
最终,这个算法的时间复杂度是:O(N * loglogN)
上面是单机版的素数筛,对于 golang
来说,怎么实现并发版的素数筛呢?
参考《Go语言高级编程》 中的一段,我们有如下程序:
package main
import "fmt"
// 返回生成自然数序列的管道: 2, 3, 4, ...
func GenerateNatural() chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
ch <- i
}
}()
return ch
}
// 管道过滤器: 删除能被素数整除的数
func PrimeFilter(in <-chan int, prime int) chan int {
out := make(chan int)
go func() {
for {
if i := <-in; i%prime != 0 {
out <- i
}
}
}()
return out
}
func main() {
ch := GenerateNatural() // 自然数序列: 2, 3, 4, ...
for i := 0; i < 100; i++ {
prime := <-ch // 新出现的素数
fmt.Printf("%v: %v\n", i+1, prime)
ch = PrimeFilter(ch, prime) // 基于新素数构造的过滤器
}
}
这段程序,我开始一直不理解,为什么这样可以按序的、准确的输出素数呢?查看函数 PrimeFilter
,觉得channl
里面应该没有数字啊,怎么可以执行过滤呢?还有就是这一段:ch = PrimeFilter(ch, prime)
。
完全不可理解。
后来,主要到 串联 这两个字。还是对 ``Goroutine `不够了解。
我们看到,在函数GenerateNatural()
中,有一个 for
不断的产生数字。只要他的 channl 中的数字被取出,如:
prime := <-ch // 新出现的素数
那么,这个函数就会源源不断的产生新的数字。而且,这个协程 GenerateNatural
在不出现意外或主函数不退出的情况下,是不会中断的!!!
同理,PrimeFilter()
函数也不会中断。
那么,就意味着,每一次 for
循环时,都会增加一个 Goroutine()
!!!
我们可以使用 runtime.NumGoroutine()
,查看这个过程中开启了多少的 Goroutine()
:
func main() {
ch := GenerateNatural() // 自然数序列: 2, 3, 4, ...
for i := 0; i < 5; i++ {
prime := <-ch // 新出现的素数
//fmt.Printf("%v: %v\n", i+1, prime)
fmt.Printf("开启的 Goroutine 数: %d\n",runtime.NumGoroutine())
ch = PrimeFilter(ch, prime) // 基于新素数构造的过滤器
}
}
####输出
开启的 Goroutine 数: 2
开启的 Goroutine 数: 3
开启的 Goroutine 数: 4
开启的 Goroutine 数: 5
开启的 Goroutine 数: 6
也就是说,通道从
GenerateNatural() --> PrimeFilter(ch1, prime) --> PrimeFilter(ch2, prime) --> PrimeFilter(ch.., prime)
一层一层的过滤。也就是图:
另外,更加优雅的推出,我们可以使用
context包