本科刚学编程的时候经常遇到判断一个数是不是素数之类的编程题,当时基础比较差,用的方法也相当简单粗暴,直接采用枚举法判断,虽然简单但是效率很低,时间复杂度为 O(n^2) ,后来慢慢了解了神奇的埃氏筛法,只用 O(nlglgn) 的时间复杂度就可以完成,效率大幅度提高,如果对埃氏筛不了解的童鞋可以看看这篇文章 sieve of Eratosthenes。

一般的埃氏筛实现都是单线程的,并不能完全发挥现在多核 CPU 的全部性能,下面给出了 Golang 并发版本的埃氏筛实现。
1 | func generate(ch chan int) { |
1 | func filter(in, out chan int, prime int) { |
1 | func main() { |
详解
并发版本的埃氏筛代码行数虽然不多,然而实现还是比较 tricky 的,谁要是第一遍就看懂算我输(你要是天才的话当我没说过)。

首先要起一个 goroutine ,记为 G , 用于生成2以后的整数,并放到通道中(记为 out0 )供别的 goroutine 读取。
1 | ch := make(chan int) |
进入第一个 for 循环,从上面的通道中取出第一个数2,毫无疑问,2为素数,所以创建一个通道 out1 ,并起一个 goroutine ,记为 filter1 ,用于过滤所有2的整数倍的数,此时通道数据流的走向为 G -> out1 ,只有通过 filter1 筛选的数才有资格进入 out1 中,此时将 ch 设为 out1 ( ch 用于记录该链上的最后一个通道),第一个循环结束。
1 | prime := <-ch // <-ch |
进入第二个 for 循环,因为此时 ch 为 out1 ,尝试去 out1 中取,3由 G 产生,然后经过 filter1 后成功进入 out1 ,说明3是素数,此时我们再创建一个通道 out2 ,并且再起一个 goroutine ,记为 filter2 ,该 goroutine 会过滤所有3的整数倍的数,此时通道数据流的走向为 G -> out1 -> out2 ,只有成功经过 filter1,filter2 筛选的数才能进入通道 out2 中,将 ch 设为 out2 ,第二个循环结束。当G 中产生4,并尝试在这个链上移动时,当它经过 filter1 时,由于4是2的整数倍,说明不是素数,不符合要求,4出局。
1 | prime := <-ch // <-out1 |
进入第三个 for 循环,同样的,此时 ch 为 out2 ,尝试去 out2 中取,5由 G 产生,经过 filter1,filter2 后成功进入 out2 ,说明5为素数,此时我们再创建一个通道 out3 ,并且再起一个 goroutine ,记为 filter3 ,该 goroutine 会过滤所有5的整数倍的数,此时通道数据流的走向为 G -> out1 -> out2 -> out3 ,只有成功经过 filter1,filter2,filter3 筛选的数才能进入通道 out3 中,将 ch 设为 out3 ,第三个循环结束。当 G 中产生6,并尝试在这个链上移动时,经过 filter1 时,由于6是2的整数倍,说明不是素数,不符合要求,同样出局。
1 | prime := <-ch // <-out2 |
进入第四个 for 循环,此时 ch 为 out3 ,尝试去 out3 中取,7由 G 产生,然后经过 filter1,filter2,filter3 筛选后成功进入 out3 ,说明7是素数,此时我们再创建一个通道 out4 ,并且再起一个 goroutine ,记为 filter4 ,该 goroutine 会过滤所有7的整数倍的数,此时通道数据流的走向为 G -> out1 -> out2 -> out3 -> out4 ,只有成功经过 filter1,filter2,filter2,filter4 筛选的数才能进入通道 out4 中,将 ch 设为 out4 ,第四个循环结束。
1 | prime := <-ch // <-out3 |
以此类推,并发版本素数筛的本质就是构建 DaisyChain ,每个数经过 DaisyChain 层层筛选后,如果最终保留,说明该数为素数,作为 DaisyChain 的最后一层加入,如此往复,就可以达到快速筛选素数的目的了。
chain of gophers
上面这种将多个通道串联起来形成菊花链的模式还是比较常见,代码的关键就在于对通道指针的理解,例如下面的例子中就指定了 left 和 right 两个通道”指针”,最初这两个”指针”都指向 leftmost 通道,每次新建一个通道时候,都将 left 重新指向 right ,最后向 right 通道中发送0,leftmost 通道也就会很快收到最终的一个值了。
1 | +----+ +----+ +----+ +----+ +----+ |
1 | func f(left, right chan int) { |