Polar to Cartesian—Concurrency
Go语言很重要的一方面是,它可以充分利用多处理器和多核计算机。许多Go并发程序在写的时候根本不需要使用到锁。
Go语言中有两个特点可以让并发编程也是一种乐趣。 第一个,goroutines(实际上是非常轻量的threads/coroutines)可以很容易的创建,而无需继承某些"thread"类。 第二个, channels在两个goroutines之间,提供了类型安全的单向或者双向的通信,用于goroutines之间的同步.
Go语言的并发是在传达数据,而不是分享数据。这就使得在Go中写并发程序相对于传统的线程和锁方法更加容易。由于不是使用分享数据,我们不会获得竞态条件(比如死锁)。并且我们不需要记得上锁和锁,因为这里没有分享的数据需要保护。
本节的例子程序中使用了两个communication channels,并且将程序的处理逻辑放在了独立的goroutine. 对于一个小的程序来说,这样写就态复杂了。 但它确可以清晰和简短的方式说明Go特性的基本使用。 更多趋于真实性的例子可以参考第7章的Go channels和goroutines.
这个程序我们称为polar2cartesian; 它是一个控制台交互程序,提示用户输入两个以空格分隔的数字,一个是半径,一个是角度——然后程序使用这两个数字计算出笛卡儿坐标。除了讲了并发,它也展示了简单的struct, 以及如何确定程序是否运行在Unix-like系统或者Windows系统。以下是这个例子运行在Linux console上:
$ ./polar2cartesian
Enter a radius and an angle (in degrees), e.g., 12.5 90, or Ctrl+D to quit.
Radius and angle: 5 30.5
Polar radius=5.00 θ=30.50° → Cartesian x=4.31 y=2.54
Radius and angle: 5 -30.25
Polar radius=5.00 θ=-30.25° → Cartesian x=4.32 y=-2.52
Radius and angle: 1.0 90
Polar radius=1.00 θ=90.00° → Cartesian x=-0.00 y=1.00
Radius and angle: ^D
$
此程序是在polar2cartesian/polar2cartesian.go, 我们将从上往下看,首先是imports, 然后是struct使用,然后是init()函数,然后是main()函数,然后是在main()函数中调用的函数。
import (
"bufio"
"fmt"
"math"
"os"
"runtime"
)
polar2cartesian程序导入多个包,许多我们之前已经讲解过,在这里我们只讲解新的包。math package提供了对浮点数的数学计算函数。runtime package提供了程序运行时的属性,比如程序运行时的平台。
type polar struct {
radius float64
θ float64
}
type cartesian struct {
x float64
y float64
}
在Go中,struct是一种类型,它保存一个或者多个数据字段. 这些字段可以是内置的类型(float64), 或者structs, 或者interface, 或者这些的组合。(一个接口类型的数据字段,实际上是指向到一个元素的指针——可以是任何类型,只要满足这个接口).
虽然两个struct的数据字段的类型是一样的,但两者之间可不可以自动转换,这原于防错性程序设计。毕竟, 将一个简单的用一个笛卡尔坐标代替一个极坐标没有什么意义。对于这样的情况,我们简单的创建一个转换方法(i.e, 其中一个类型有一个方法,用来返回另一个类型),它利用Go的复合文字语法(composite literal syntax)创建一个目标类型的值,并将源类型的字段填充到这个创建的值。
var prompt = "Enter a radius and an angle (in degrees), e.g., 12.5 90, " +
"or %s to quit."
func init() {
if runtime.GOOS == "windows" {
prompt = fmt.Sprintf(prompt, "Ctrl+Z, Enter")
} else { // Unix-like
prompt = fmt.Sprintf(prompt, "Ctrl+D")
}
}
如果一个package中有一个或者多个init()函数,它们会在main package的main()函数调用之前自动执行。所以当我们的polar2cartesian程序被调用时,这个init()函数第一个被调用。 我们使用init()来设置提示信息,如何在不同平台关闭这个应用程序。比如在Windows下,按Ctrl+Z, 然后回车。runtime package的GOOS(Go Operating System)常量是一个字符串,表明当前程序运行的操作系统。典型的值为darwin(Max OSX), freebsd, linux and windows.
在讨论main函数和程序剩余部分前,我们先简单的讨论一下channels并且在正式使用channels前先看一些小(玩具的)例子。
Channels是仿制的Unix pipe, 并且提供双向(或者单向)的数据通信。Channels的行为跟FIFO队列很像(先进,先出),因此可以保证发送的顺序。不能从channel中删除元素,但我们可以在接收端忽略部分或者全部的元素。让我们看一个非常简单的例子,首先创建一个channel.
messages := make(chan string, 10)
Channels的创建是使用make()函数,使用chan Type 的语法。我们在这创建了一个messages channel用来发送和接收字符串. 第二个参数是缓冲区的大小(默认为0); 这里我们创建的channel可以接受10个字符串。如果一个channel缓冲区被填满,,它将被阻塞直到从中接收了一个元素。这个意思是任意数量的元素可以通过一个channel传递,从channel检索出元素,可以后续的元素让出空间。一个channel的缓冲区为0,只可以用来发送一个元素。
现在我们将发送多个字符串到channel
messages <- "Leader"
messages <- "Follower"
当<-通信操作符用于二元运算时,它左边的操作数必须是一个channel. 右边的操作是发送到channel中的值,值的类型channel声明时指定.
message1 := <-messages
message2 := <-messages
当<-通信操作符被用于一元操作,只需要右边一个操作数(必须是channel), 它作为一个receiver,阻塞,直到它有一个值返回。这里,我们从messages channel中检索两个消息。 message1被赋值Leader, message2为Follower; 两个变量为string类型。
通常来说,channels是为两个goroutines之间通信而创建的。Channels的发送和接收不需要阻塞,channel阻塞行为可以用于达到同步的目的。
我们已经看了一些channel的基础使用,让我们在看看channels和goroutines在实际中的使用。
func main() {
questions := make(chan polar)
defer close(questions)
answers := createSolver(questions)
defer close(answers)
interact(questions, answers)
}
init()函数返回后,Go的运行时系统就会调用main packages的main()函数。
这里,在main()函数在开始时创建了一个channel(of type chan polar)用来传递polar structs, 并把它赋值给questions变量。 一旦channels被创建,我们使用defer语句调用内置的close()函数,以保证channel不在被需要的时候关闭。 下一步,我们调用createSolver函数,createSolver接收一个questions channel, 并返回一个answers channel(chan cartesian).
func createSolver(questions chan polar) chan cartesian {
answers := make(chan cartesian)
go func() {
for {
polarCoord := <-questions
θ := polarCoord.θ * math.Pi / 180.0 // degrees to radians
x := polarCoord.radius * math.Cos(θ)
y := polarCoord.radius * math.Sin(θ)
answers <- cartesian{x, y}
}
}()
return answers
}
createSolver()函数在开始处创建一个answers channel, 用于发送问题的答案,而这个问题是从questions channel中获得。
在创建完channel之后,是一条go语句. go statement是一个函数调用(跟defer类似), 这个函数将在独立的goroutine上以异步的方式运行。 这个意思就是,当前函数的控制流(i.e., in the main goroutine) 将立即从之后的语句中继续。在这个例子中,go statements跟随的是一条return语句,用于返回answers channel给它的调用者。
在go statement我们调用了一个匿名函数。 这个函数是一个无限循环,等待(阻塞它自己所在的goroutine, 而不是其它的goroutines,也不是这个函数开始所在的goroutine.)直到它从questions channels中接收了一个问题。 当接收了一个polar coordinate后,它开始计算,并且将计算的结果发送到answer channel.
一旦调用createSolver(),在它返回值的地方,我们有建立了两个channels和一个单独的goroutine,它阻赛并等待polar coordinates被发送到questions channel——而任何其它的goroutine不会被阻塞,包括正在执行的main().
const result = "Polar radius=%.02f θ=%.02f° → Cartesian x=%.02f y=%.02f\n"
func interact(questions chan polar, answers chan cartesian) {
reader := bufio.NewReader(os.Stdin)
fmt.Println(prompt)
for {
fmt.Printf("Radius and angle: ")
line, err := reader.ReadString('\n')
if err != nil {
break
}
var radius, θ float64
if _, err := fmt.Sscanf(line, "%f %f", &radius, &θ); err != nil {
fmt.Fprintln(os.Stderr, "invalid input")
continue
}
questions <- polar{radius, θ}
coord := <-answers
fmt.Printf(result, radius, θ, coord.x, coord.y)
}
fmt.Println()
}
这个函数接收两个channels. 在函数开始的地方为os.Stdin创建了一个缓冲读取器(buffered reader), 这是因为我们的数据是从控制台输入的。 然后它打印了使用信息,以及如何退出。
然后开始一个无限循环,它提示用户输入polar coordinate(半径和角度). 之后就是要求用户输入, 这个函数后等待用户输入一些文本,然后回车,或者Ctrl+D(or Ctrl+Z, Enter on Windows).
我们创建了两个float64s用于保存用户的输入, 然后使用 fmt.Sscanf()函数分析一行, 这个函数接用一个字符串,这个字符串被用于解析。 一个format——在这里由空格分隔的两个浮点数——以及一个或者多个要被填充的变量(&取址操作符用于获得这个值的指针). 这个函数将返回解析成功的元素个数和一个error(or nil)。如果发生错误,我们将输出一个错误信息到os.Stderr——即使程序的os.Stdout被转向到一个文件,错误信息也会显示在控制台。
如果输入的数字是有效的,并且发送给了questions channel。 我们 阻塞main goroutine,以等待answers channel中的响应。这个附加的goroutine是在createSolver()函数中创建的,并且创建后就被阻塞,以等待question channel中的polar. 所以当我们发送了polar, 附件的goroutine执行计算,将计算的结果cartesian发送到answers channel, 然后又等待另一个问题的到达。 一旦在interact()函数中从answers channel中接收了cartesian。interact()不在被阻塞。这时我们使用fmt.Printf()函数。