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 开发工具而已,中庸普通且平凡。

    即便在某些方面,它确实很方便,甚至是利器。

posted @ 2014-11-06 18:16  ecofast  阅读(828)  评论(0编辑  收藏  举报