Merge pull request #196 from glight2000/master

15.4 14.0 14.1
This commit is contained in:
Unknwon
2015-12-24 20:35:55 -05:00
6 changed files with 230 additions and 1 deletions

View File

@@ -34,3 +34,4 @@ Golang 编程245386165
|2015-11-25|13.10 性能调试:分析并优化 Go 程序
|2015-12-10|15.0 15.1 15.2
|2015-12-12|15.3
|2015-12-22|15.4

View File

@@ -1,7 +1,13 @@
# 14 协程goroutine与通道channel
作为一门21世纪的语言Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算,参 15 章和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是 协程 (goroutine) 与通道 (channel)。他们需要语言编译器和runtime的支持。Go 语言提供的垃圾回收器对并发编程至关重要。
作为一门21世纪的语言Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算,参 15 章和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是 协程 (goroutine) 与通道 (channel)。他们需要语言编译器和runtime的支持。Go 语言提供的垃圾回收器对并发编程至关重要。
*不要通过共享内存来通信,而通过通信来共享内存。*
通信强制协作。
## 链接
- [目录](directory.md)
- 上一节:[性能调试:分析并优化 Go 程序](13.10.md)
- 下一节:[并发,并行和协程](14.1.md)

156
eBook/14.1.md Normal file
View File

@@ -0,0 +1,156 @@
# 14.1 并发,并行和协程
## 14.1.1 什么是协程
一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。几乎所有'正式'的程序都是多线程的以便让用户或计算机不必等待或者能够同时服务多个请求如Web服务器或增加性能和吞吐量例如通过对不同的数据集并行执行代码。一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务但是只有在同一个程序在某一个时间点在多个些处理内核或处理器上同时执行的任务才是真正的并行。
并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。
公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作`竞态`
!!不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。
解决之道在于同步不同的线程对数据加锁这样同时就只有一个线程可以变更数据。在Go的标准库`sync`中有一些工具用来在低级别的代码中实现加锁;我们在章节[9.3](9.3.md)中讨论过这个问题。不过过去的软件开发经验告诉我们这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程:"`thread-per-connection`"模型不够有效。
Go更倾向于其他的方式在诸多比较合适的范式中有个被称作`Communicating Sequential Processes顺序通信处理`(CSP, C. Hoare发明的)还有一个叫做`message passing-model消息传递`已经运用在了其他语言中比如Eralng
在Go中应用程序并发处理的部分被称作`goroutines协程`它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系协程是根据一个或多个线程的可用性映射多路复用执行于在他们之上的协程调度器在Go运行时很好的完成了这个工作。
协程工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以使用`sync`包来实现(参见章节[9.3](9.3.md))不过我们很不鼓励这样做Go使用`channels`来同步协程(可以参见[14.2](14.2.md)等章节)
当系统调用比如等待I/O阻塞协程时其他协程会继续在其他线程上工作。协程的设计隐藏了许多线程创建和管理方面的复杂工作。
协程是轻量的比线程更轻。它们痕迹非常不明显使用少量的内存和资源使用4K的栈内存就可以在堆中创建它们。因为创建非常廉价必要的时候可以轻松创建并运行大量的协程在同一个一个地址空间中100,000个连续的协程。并且它们对栈进行了分割从而动态的增加或缩减内存的使用栈的管理是自动的但不是由垃圾回收器管理的而是在协程退出后自动释放。
协程可以运行在多个操作系统线程之间也可以运行在线程之内让你可以很小的内存占用就可以处理大量的任务。由于操作系统线程上的协程时间片你可以使用少量的操作系统线程就能拥有任意多个提供服务的协程而且Go运行时可以聪明的意识到哪些协程被阻塞了暂时搁置它们并处理其他协程。
存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序。Go的协程和通道理所当然的支持确定性的并发方式例如通道具有一个sender和一个receiver。我们会在章节[14.7](14.7.md)中使用一个常见的算法问题(工人问题)来对比两种处理方式。
协程是通过使用关键字`go`调用执行一个函数或者方法来实现的也可以是匿名或者lambda函数。这样会在当前的计算过程中开始一个同时进行的函数在相同的地址空间中并且分配了独立的栈比如`go sum(bigArray)`// 在后台计算总和
协程的栈会根据需要进行伸缩,不出现栈溢出;开发者不需要关心栈的大小。当协程结束的时候,它会静默退出:用来启动这个协程的函数不会得到任何的返回值。
任何Go程序都必须有的`main()`函数也可以看做是一个协程,尽管它并没有通过`go`来启动。协程可以在程序初始化的过程中运行(在`init()`函数中)。
在一个协程中,比如它需要进行非常密集的运算,你可以在运算循环中周期的使用`runtime.Gosched()`:这会让出处理器,允许运行其他协程;它并不会使当前协程挂起,所以它会自动恢复执行。使用`Gosched()`可以使计算均匀分布,使通信不至于迟迟得不到响应。
## 14.1.2 并发和并行的差异
Go的并发原语提供了良好的并发设计基础表达程序结构以便表示独立地执行的动作所以Go的的重点不在于并行的首要位置并发程序可能是并行的也可能不是。并行是一种通过使用多处理器以提高速度的能力。但往往是一个设计良好的并发程序在并行方面的表现也非常出色。
在当前的运行时2012年一月实现中Go默认没有并行指令只有一个独立的核心或处理器被专门用于Go程序不论它启动了多少个协程所以这些协程是并发运行的但他们不是并行运行的同一时间只有一个协程会处在运行状态。
这个情况在以后可能会发生改变,不过届时,为了使你的程序可以使用多个核心运行,这时协程就真正的是并行运行了,你必须使用`GOMAXPROCS`变量。
这会告诉运行时有多少个协程同时执行。
并且只有gc编译器真正实现了协程适当的把协程映射到操作系统线程。使用`gccgo`编译器,会为每一个协程创建操作系统线程。
## 14.1.3 使用GOMAXPROCS
在gc编译器下6g或者8g你必须设置GOMAXPROCS为一个大于默认值1的数值来允许运行时支持使用多于1个的操作系统线程所有的协程都会共享同一个线程除非将GOMAXPROCS设置为一个大于1的数。当GOMAXPROCS大于1时会有一个线程池管理许多的线程。通过`gccgo`编译器GOMAXPROCS有效的与运行中的协程数量相等。假设n是机器上处理器或者核心的数量。如果你设置环境变量GOMAXPROCS>=n或者执行`runtime.GOMAXPROCS(n)`接下来协程会被分割分散到n个处理器上。更多的处理器并不意味着性能的线性提升。有这样一个经验法则对于n个核心的情况设置GOMAXPROCS为n-1以获得最佳性能也同样需要遵守这条规则协程的数量 > 1 + GOMAXPROCS > 1
所以如果在某一时间只有一个协程在执行不要设置GOMAXPROCS
还有一些通过实验观察到的现象在一台1颗CPU的笔记本电脑上增加GOMAXPROCS到9会带来性能提升。在一台32核的机器上设置GOMAXPROCS=8会达到最好的性能在测试环境中更高的数值无法提升性能。如果设置一个很大的GOMAXPROCS只会带来轻微的性能下降设置GOMAXPROCS=100使用“top”命令和“H”选项查看到只有7个活动的线程。
增加GOMAXPROCS的数值对程序进行并发计算是有好处的
请看[goroutine_select2.go](examples/chapter_14/goroutine_select2.go)
总结GOMAXPROCS等同于并发的线程数凉在一台核心数多于1个的机器上会尽可能有等同于核心数的线程在并行运行。
## 14.1.4 如何用命令行指定使用的核心数量
使用`flags`包,如下:
```go
var numCores = flag.Int("n", 2, "number of CPU cores to use")
in main()
flag.Pars()
runtime.GOMAXPROCS(*numCores)
```
协程可以通过调用`runtime.Goexit()`来停止,尽管这样做几乎没有必要。
示例 14.1-[goroutine1.go](examples/chapter_14/goroutine1.go) 介绍了概念:
```go
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("In main()")
go longWait()
go shortWait()
fmt.Println("About to sleep in main()")
// sleep works with a Duration in nanoseconds (ns) !
time.Sleep(10 * 1e9)
fmt.Println("At the end of main()")
}
func longWait() {
fmt.Println("Beginning longWait()")
time.Sleep(5 * 1e9) // sleep for 5 seconds
fmt.Println("End of longWait()")
}
func shortWait() {
fmt.Println("Beginning shortWait()")
time.Sleep(2 * 1e9) // sleep for 2 seconds
fmt.Println("End of shortWait()")
}
```
输出
```
In main()
About to sleep in main()
Beginning longWait()
Beginning shortWait()
End of shortWait()
End of longWait()
At the end of main() // after 10s
```
`main()``longWait()``shortWait()`三个函数作为独立的处理单元按顺序启动,然后开始并行运行。每一个函数都在运行的开始和结束阶段输出了消息。为了模拟他们运算的时间消耗,我们使用了`time`包中的`Sleep`函数。`Sleep()`可以按照指定的时间来暂停函数或协程的执行这里使用了纳秒ns符号1e9表示1乘10的9次方e=指数)。
他们按照我们期望的顺序打印出了消息,几乎都一样,可是我们明白这是模拟出来的,以并行的方式。我们让`main()`函数暂停10秒从而确定它会在另外两个协程之后结束。如果不这样如果我们让`main()`函数停止4秒`main()`会提前结束,`longWait()`则无法完成。如果我们不在`main()`中等待,协程会随着程序的结束而消亡。
`main()`函数返回的时候程序退出它不会等待任何其他非main协程的结束。这就是为什么在服务器程序中每一个请求都会启动一个协程来处理`server()`函数必须保持运行状态。通常使用一个无限循环来达到这样的目的。
另外,协程是独立的处理单元,一旦陆续启动一些协程,你无法确定他们是什么时候真正开始执行的。你的代码罗技必须具独立于协程调用的顺序。
为了对比使用一个线程连续调用的情况移除go关键字重新运行程序。
现在输出:
```
In main()
Beginning longWait()
End of longWait()
Beginning shortWait()
End of shortWait()
About to sleep in main()
At the end of main() // after 17 s
```
协程更有用的一个例子应该是在一个非常长的数组中查找一个元素。
将数组分割为若干个不重复的切片,然后给每一个切片启动一个协程进行查找计算。这样许多并行的线程可以用来进行查找任务,整体的查找时间会缩短(除以协程的数量)。
## 14.1.5 Go协程goroutines和协程coroutines
译者注标题中的“Go协程goroutines”即是14章讲的协程指的是go语言中的协程。而“协程coroutines”指的是其他语言中的协程概念仅在本节出现。
在其他语言中比如C#Lua或者Python都有协程的概念。这个名字表明它和Go协程有些相似不过有两点不同
* go协程意味着并行或者可以以并行的方式部署协程一般来说不是这样的
* go协程通过通道来通信协程通过让出和恢复操作来通信
Go协程比协程更强大也很容易从协程的逻辑复用到go协程。
## 链接
- [目录](directory.md)
- 上一节:[性能调试:分析并优化 Go 程序](13.10.md)
- 下一节:[并发,并行和协程](14.1.md)

View File

@@ -0,0 +1,60 @@
# 15.4 写一个简单的网页应用
下边的程序在端口8088上启动了一个网页服务器`SimpleServer`会处理`/test1`url使它在浏览器输出`hello world``FormServer`会处理'/test2`url如果url最初由浏览器请求那么它就是一个`GET`请求,并且返回一个`form`常量,包含了简单的`input`表单,这个表单里有一个文本框和一个提交按钮。当在文本框输入一些东西并点击提交按钮的时候,会发起一个`POST`请求。`FormServer`中的代码用到了`switch`来区分两种情况。在`POST`情况下,使用`request.FormValue("inp")`通过文本框的`name`属性`inp`来获取内容并写回浏览器页面。在控制台启动程序并在浏览器中打开url`http://localhost:8088/text2`来测试这个程序:
示例 15.10 [simple_webserver.go](examples/chapter_15/simple_webserver.go)
```go
package main
import (
"io"
"net/http"
)
const form = `
<html><body>
<form action="#" method="post" name="bar">
<input type="text" name="in" />
<input type="submit" value="submit"/>
</form>
</body></html>
`
/* handle a simple get request */
func SimpleServer(w http.ResponseWriter, request *http.Request) {
io.WriteString(w, "<h1>hello, world</h1>")
}
func FormServer(w http.ResponseWriter, request *http.Request) {
w.Header().Set("Content-Type", "text/html")
switch request.Method {
case "GET":
/* display the form to the user */
io.WriteString(w, form)
case "POST":
/* handle the form data, note that ParseForm must
be called before we can extract form data */
//request.ParseForm();
//io.WriteString(w, request.Form["in"][0])
io.WriteString(w, request.FormValue("in"))
}
}
func main() {
http.HandleFunc("/test1", SimpleServer)
http.HandleFunc("/test2", FormServer)
if err := http.ListenAndServe(":8088", nil); err != nil {
panic(err)
}
}
```
当使用字符串常量表示html文本的时候包含`<html><body></body></html>`对于让浏览器识别它收到了一个html非常重要。
更安全的做法是在处理器中使用`w.Header().Set("Content-Type", "text/html")`在写入返回之前将`header`的`content-type`设置为`text/html`
`content-type`会让浏览器认为它可以使用函数`http.DetectContentType([]byte(form))`来处理收到的数据
练习 15.6 [statistics.go]
编写一个网页程序,可以让用户输入一连串的数字,然后将它们打印出来,计算出这些数字的均值和中值,就像下边这张截图一样:
![](../images/15.4_fig15.1.jpg?raw=true)

View File

@@ -136,8 +136,14 @@
- 13.9 [用(测试数据)表驱动测试](13.9.md)
- 13.10 [性能调试:分析并优化 Go 程序](13.10.md)
- 第14章[协程goroutine与通道channel](14.0.md)
- 14.1 [并发,并行和协程](14.1.md)
- 14.2 [使用通道进行协程间通信](14.2.md)
- 14.3 [协程同步:关闭通道-对阻塞的通道进行测试](14.3.md)
- 第15章[网络、模版与网页应用](15.0.md)
- 15.1 [tcp服务器](15.1.md)
- 15.2 [一个简单的web服务器](15.2.md)
- 15.3 [访问并读取页面数据](15.3.md)
- 15.4 [写一个简单的网页应用](15.4.md)
## 第四部分:实际应用

BIN
images/15.4_fig15.1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB