Life is short
相信不少码农曾看过类似“life is short, use Python”等之类略带调侃意味的小段子(譬如我),而其也并非不无道理。每门编程语言都是合理的存在,都有它们的优点,及缺陷。
码农们也大多学过用过不止一门语言,譬如我。
像我这样曾胡摸过 N 门语言,但只会勉强熟练使用其中两三门或三四门的码农(C++、C#、Delphi、Golang),却还是想浅浅地比较一下它们,算是一点皮毛心得吧。
从这个偶然看到的例子开始。
uses SyncObjs, System.Threading, System.Diagnostics; {function local to the unit} function IsPrime (N: Integer): Boolean; var Test: Integer; begin IsPrime := True; for Test := 2 to N - 1 do if (N mod Test) = 0 then begin IsPrime := False; break; {jump out of the for loop} end; end; const Max = 100000; // 100K procedure TFormParallelFor.btnPlainForLoopClick(Sender: TObject); var I, Tot: Integer; Ticks: Cardinal; begin // counts the prime numbers below a given value Tot := 0; Ticks := GetTickCount; for I := 1 to Max do begin if IsPrime (I) then Inc (Tot); //Application.ProcessMessages; end; Ticks := GetTickCount - Ticks; Memo1.Lines.Add (Format ( 'Plain for: %d - %d', [Ticks, Tot])); end; procedure TFormParallelFor.btnParallelForLoopClick(Sender: TObject); var Tot: Integer; Ticks: Cardinal; begin Tot := 0; Ticks := GetTickCount; TParallel.For(1, Max, procedure (I: Int64) begin if IsPrime (I) then InterlockedIncrement (Tot); end); Ticks := GetTickCount - Ticks; Memo1.Lines.Add (Format ( 'Parallel for: %d - %d', [Ticks, Tot])); end;
Delphi XE7 新引入了一个并发辅助库(似乎由 Allen Bauer 操的刀),针对多核编程提供了语言层面上的支持。刚好在逛 Embarcadero 论坛的时候看到了这篇浅显的文章,得知 Delphi 是通过其自实现的可自动伸缩、非常智能的线程池的方式来实现对并发的支持,其中有一条定义引起了我的注意:MaxThreadsPerCPU = 25(当然我还不清楚为何每个核上挂的最大线程数量是 25)。我已懒得去下载去安装庞大的 XE7 去看其相关的 RTL 源码了,但既然我刚好又有个官方的小 Demo,索性跑跑看,如下。
看上去确实,这个并行库是比较不错的。
于是我有点心痒了,不知如果用 Golang 来做同样的实现,速度会如何呢?如下(注:因只想单纯测试 goroutine 和 channel,所以实现代码没有使用原子操作包 sync/atomic 提供的功能。而我并不熟 OpenMP,所以也没作这方面的对比)。
// ParallelCalcDemo project main.go package main import ( "fmt" "runtime" "time" ) const cNumMax = 100000 func IsPrime(num int) bool { ret := true for i := 2; i < num; i++ { if num%i == 0 { ret = false break } } return ret } func plainCalc() { cnt := 0 t1 := time.Now() for i := 1; i <= cNumMax; i++ { if IsPrime(i) { cnt++ } } t2 := time.Now() fmt.Printf(" plainCalc - Counts: %d; Time: %dms\n", cnt, t2.Sub(t1)/time.Millisecond) } func parallelHandle(numMin, numMax int, ch chan int) { cnt := 0 for i := numMin; i <= numMax; i++ { if IsPrime(i) { cnt++ } } ch <- cnt } func parallelCalc(numGoroutines int) { t1 := time.Now() chans := make(chan int, numGoroutines) seg := cNumMax / numGoroutines if cNumMax%numGoroutines != 0 { seg++ } for i := 0; i < numGoroutines-1; i++ { go parallelHandle(1+i*seg, (i+1)*seg, chans) } go parallelHandle(1+(numGoroutines-1)*seg, cNumMax, chans) cnt := 0 for i := 0; i < numGoroutines; i++ { cnt += <-chans } t2 := time.Now() fmt.Printf(" parallelCalc - Counts: %d; Time: %dms\n", cnt, t2.Sub(t1)/time.Millisecond) } func parallelHandle2(value int, ch chan int) { if IsPrime(value) { ch <- 1 return } ch <- 0 } func parallelCalc2() { t1 := time.Now() chans := make(chan int, cNumMax) for i := 1; i <= cNumMax; i++ { go parallelHandle2(i, chans) } cnt := 0 for i := 1; i <= cNumMax; i++ { cnt += <-chans } t2 := time.Now() fmt.Printf("parallelCalc2 - Counts: %d; Time: %dms\n", cnt, t2.Sub(t1)/time.Millisecond) } func main() { numGoroutines := runtime.NumCPU() runtime.GOMAXPROCS(numGoroutines) plainCalc() parallelCalc(numGoroutines) parallelCalc2() }
小跑 10 次,结果如下。
可见效率也还是可以的,虽然比 Delphi 的并行版本要慢几个百分点。只是既然前文有提到 25 这个数字,我想为何不尝试下将 GOMAXPROCS 设为 runtime.NumCPU() * 25 呢?如是略作改动之前的代码,得到如下结果。
于是竟然发现,(并行)效率似乎比修改前的要高一点,跟 Delphi 的并行版本基本差不多了。综合网上的一些信息,似乎 Golang 的调度器还是不够“NB”:)
当然,譬如对于 parallelCalc2 版本的实现,可能只是因开了 100K 个 goroutine 后引来的丁点调度效率损失而已。
但即便如此,即便只是通过上面简单的代码片段,我们还是可以看到,在 Golang 里实现并发实在是太方便了,goroutine 和 channel 实在是非常廉价却又非常简洁高效的并发利器。而这样的特性对于服务器端开发来说同样非常有用且重要,很大程度上简化了服务器端的多线程相关逻辑。在语言核心层面支持并行和分布式,并发模型简洁高效,简直是天生的后端开发牛刀。
譬如如下的代码片段,正是其威力的小小体现。
// SimpleTcpServer project main.go package main import ( "fmt" "net" "strings" ) func main() { fmt.Println("Starting the server...") listener, err := net.Listen("tcp", "localhost:50000") if err != nil { fmt.Println("Error listening", err.Error()) return } for { conn, err := listener.Accept() if err != nil { fmt.Println("Error accepting", err.Error()) return } go doStuff(conn) } } func doStuff(conn net.Conn) { for { buf := make([]byte, 512) _, err := conn.Read(buf) if err != nil { fmt.Println("Error reading", err.Error()) return } fmt.Printf("Received data: %v", strings.Trim(string(buf), " ")) } }
所以,Golang 还是有点意思的,对吧?
刚开始学习 Golang 时,其似乎有点“反人类”的语法曾让我比较不适应(估计不少码农或多或少也会如此感觉),但熟悉一段时间后居然也算很快就看顺眼了。其譬如 comma, ok form、类型推导(当然 C++11 有 auto,C# 也有这语法特性)、延迟执行等语法糖特性确实比较简洁,但同时却功能强大且实用,写惯了 try..catch / try...except...finally等类似代码、习惯了通过各种手段来释放清除资源的我们,在遇到 comma, ok、defer 等这样非常简洁的语法时,会不会有惊喜甚至拍案叫绝的感受呢(对于我,好像有过)?
顺便提一句,C# 里的 try..except..finally 类似于 Delphi 的相关语法,但不同的是后者却只有 try..except 和 try..finally,一直都没有加入前者这样的增强特性,我有点想不通。
我们习惯了输入/遇到类似 private、public、protected 等所谓关键字,甚至更甚,internal、protected internal 等,而 Golang 仅以首字符大小写来控制访问权限实现相同效果,不能不说比较新颖、简洁。有时候我居然会想,譬如 C++ / C# 引入了许多确实看起来实用的语法糖,但它们很少甚至极少用到,且绝大多数完全可以由用户(即码农)改进其设计的方式来实现,而既如此,为什么要引入那些呢,它们真的对所谓的软件工程有比较明显的益处?
C++ 有那么多的语言语法特性,但扪心自问,我们实际开发中真正用到了多少?大多数时候我们都只是使用其很小的一部分子集而已。而其本身的语言复杂度,带来了大幅提升的学习及使用成本,带来了非常复杂冗长的编译过程,也带来了(不合格)程序员滥用某些特性的可能。即便它能生成非常高效(及紧凑的)的机器码,但这背后的成本实在太大了。
再顺便提一句,说到 private、public 等,Delphi 的 published 特性确实对写 UI 相关的程序时非常好用且很实用,相信用过 Delphi 的码农会有体会(C# 码农同理)。
用 C++ 做 UI?唉。
getter / setter? property 多么好的包装了它们。
我曾想在 C++ 里模拟 Delphi 的 initialization 和 finalization 特性,但却没能很好的如愿(当然,C++Builder 有这个特性)。但 Golang 提供了 init,这样的语言设计细节,还是带给我不少欢喜的(Golang 有 GC,所以 finalization 存在的意义不大)。
然后是譬如代码格式化等算不上语言特性的特性,我个人还是感觉比较有意义且实用的,尤其是对于有所谓代码洁癖的码农来说:)
同样,无需分号结束每行代码等特性,确是有点实际意义的,而这些点滴的语言特性累积,才能便于使用它的码农写出简洁、直观的代码。
只是,虽然 Golang 的 goroutine / channel 很强大简洁,但还是需要不少实践及实例去领会理解,譬如下面这段看似简单的代码,是不是有些“抽象”:)
// primesieve project main.go package main import ( "fmt" ) // Send the sequence 2, 3, 4, ... to channel ch func generate(ch chan int) { for i := 2; ; i++ { ch <- i } } // Copy the values from channel in to channel out, removing those divisible by prime func filter(in, out chan int, prime int) { for { i := <-in if i%prime != 0 { out <- i } } } func main() { ch := make(chan int) go generate(ch) for { prime := <-ch fmt.Print(prime, " ") ch1 := make(chan int) go filter(ch, ch1, prime) ch = ch1 } }
但既然 goroutine / channel 的作用是那么大,用起来又这样简洁,把花在研究奇技淫巧上的时间拿来学习领会它们,为何不可?
Life is short, try Golang。
作为曾重度使用过 Delphi 的码农,很费解为何其一直在标榜所谓的数据库开发,即便两年前移动游戏开发已开始如火如荼,它却始终没能提供,或支持其爱好者提供可用的框架(想想下 Cocos2d 吧)。这次大潮它没赶上,估计也不会赶上了。顺势太重要,错过难再有。
虽然它早已能做多平台的应用开发,但始终,它恐怕只将自身定位为所谓的应用 APP 开发工具而已,中庸普通且平凡。
即便在某些方面,它确实很方便,甚至是利器。