这是一个使用HTML5制作的幻灯片
使用 → 键开始播放。
多核.
网络.
CPU云.
海量用户.
我们要用技术解决这些问题。
这就是为什么会有并发概念的出现。
Go语言提供:
不!错了。
当Go语言发布时,很多人区分不了这两者之间的差别。
"我用4个处理器来执行素数筛选程序,但程序执行的更慢了!"
将相互独立的执行过程综合到一起的编程技术。
(这里是指通常意义上的执行过程,而不是Linux进程。很难定义。)
同时执行(通常是相关的)计算任务的编程技术。
并发是指同时处理很多事情。
而并行是指同时能完成很多事情。
两者不同,但相关。
一个重点是组合,一个重点是执行。
并发提供了一种方式让我们能够设计一种方案将问题(非必须的)并行的解决。
并发:鼠标,键盘,显示器,硬盘——同时工作。
并行:向量数量积
并发是一种将一个程序分解成小片段独立执行的程序设计方法。
通信是指各个独立的执行任务间的合作。
这是Go语言采用的模式,包括Erlang等其它语言都是基于这种SCP模式:
C. A. R. Hoare: Communicating Sequential Processes (CACM 1978)
概念太抽象。我们来点具体的。
运一堆没用的手册到焚烧炉里。
如果只有一只地鼠,这需要很长时间。
更多的地鼠还不行;他们需要更多的小推车。
这样快多了,但在装运处和焚烧炉处出现了瓶颈。
还有,这些地鼠需要能同时工作。
它们需要相互通知。(这就是地鼠之间的通信)
消除瓶颈;让他们能真正的相互独立不干扰。
这样吞吐速度会快一倍。
并发组合两个地鼠的工作过程。
现在的这种工作流程不能自动的实现并行!
如果只有一只地鼠
这仍然是并发(就是目前的这种工作方式),但它不是并行。
然而,它是可以并行的!
需要设计出另外的工作流程来实现并发组合。
三只地鼠在工作,但看起来工作有些滞后。
每只地鼠都在做一种独立的工序,
并且相互合作(通信)。
增加一只地鼠,专门运回空的小推车。
四只地鼠组成了一个优化的工作流程,每只只做自己一种简单的工序。
如果任务布置的合理,这将会比最初一个地鼠的工作快4倍。
我们通过在现有的工作流程里加入并发过程从而改进了执行效率。
地鼠越多能做的越多;工作效率越高。
这是一种比仅仅并行更深刻的认识。
四个地鼠有不同的工作环节:
不同的并发设计能导致不同的并行方式。
现在我们可以让并行再多一倍;按照现在的并行模式很容易实现这些。八个地鼠,全部繁忙。
请记住,只有一个地鼠在工作(零并行),这仍然是一个正确的并发的工作方案。
现在我们换一种设计来组织我们的地鼠的并发工作流程。
两个地鼠,一个中转站。
更多的并发流程能获得更多的吞吐量。
在每个中转站之间都引入多个地鼠并发的模式:
使用这种技术策略,16个地鼠都很繁忙!
有很多分解流程的方式。
这都是并发设计。
一旦完成了分解,并行可能会丧失,但很容易纠正。
将我们的运书工作替换成如下:
我们现在的这种设计就是一种可扩展的Web服务的并发设计。
地鼠提供Web内容服务。
这里不是一个详细的教材,只是快速做一些重点介绍。
一个Go例程就是一个和其它Go例程在同一地址空间里但却独立运行的函数。
f("hello", "world") // f runs; we wait
go f("hello", "world") // f starts running g() // does not wait for f to return
就像是在shell里使用 & 标记启动一个命令。
(很像线程,但比线程更轻量。)
多个例程可以在系统线程上做多路通信。
当一个Go例程阻塞时,所在的线程会阻塞,但其它Go例程不受影响。
通道是类型化的值,能够被Go例程用来做同步或交互信息。
timerChan := make(chan time.Time) go func() { time.Sleep(deltaT) timerChan <- time.Now() // send time on timerChan }() // Do something else; when ready, receive. // Receive will block until timerChan delivers. // Value sent is other goroutine's completion time. completedAt := <-timerChan
这select语句很像switch,但它的判断条件是基于通信,而不是基于值的等量匹配。
select { case v := <-ch1: fmt.Println("channel 1 sends", v) case v := <-ch2: fmt.Println("channel 2 sends", v) default: // optional fmt.Println("neither channel was ready") }
非常。
一个程序里产生成千上万个Go例程很正常。
(有一次调试一个程序发现有130万个例程。)
堆栈初始很小,但随着需求会增长或收缩。
Go例程不是不耗资源,但它们很轻量级的。
它让一些并发运算更容易表达。
它们是局部函数。
下面是一个非并发例子。
func Compose(f, g func(x float) float) func(x float) float { return func(x float) float { return f(g(x)) } } print(Compose(sin, cos)(0.5))
通过实例学习Go语言并发
使用闭包封装一个后台操作。
下面是从输入通道拷贝数据到输出通道。
go func() { // copy input to output for val := range input { output <- val } }()
这个for range操作会一直执行到处理掉通道内最后一个值。
数据类型:
type Work struct { x, y, z int }
一个worker的任务:
func worker(in <-chan *Work, out chan<- *Work) { for w := range in { w.z = w.x * w.y Sleep(w.z) out <- w } }
必须保证当一个worker阻塞时其他worker仍能运行。
runner:
func Run() { in, out := make(chan *Work), make(chan *Work) for i := 0; i < NumWorkers; i++ { go worker(in, out) } go sendLotsOfWork(in) receiveLotsOfResults(out) }
很简单的任务,但如果没有并发机制,你仍然很难这么简单的解决。
这个负载均衡的例子具有很明显的并行和可扩展性。
Worker数可以非常巨大。
Go语言的这种并发特征能的开发一个安全的、好用的、可扩展的、并行的软件变得很容易。
没有明显的需要同步的操作。
程序的这种设计隐含的实现了同步。
让我们实现一个更有意义的负载均衡的例子。
请求者向均衡服务发送请求。
type Request struct { fn func() int // The operation to perform. c chan int // The channel to return the result. }
注意这返回的通道是放在请求内部的。
通道是first-class值
没有实际用处,但能很好的模拟一个请求者,一个负载产生者。
func requester(work chan<- Request) { c := make(chan int) for { // Kill some time (fake load). Sleep(rand.Int63n(nWorker * 2 * Second)) work <- Request{workFn, c} // send request result := <-c // wait for answer furtherProcess(result) } }
一些请求通道,加上一些负载记录数据。
type Worker struct { requests chan Request // work to do (buffered channel) pending int // count of pending tasks index int // index in the heap }
均衡服务将请求发送给压力最小的worker。
func (w *Worker) work(done chan *Worker) { for { req := <-w.requests // get Request from balancer req.c <- req.fn() // call fn and send result done <- w // we've finished this request } }
请求通道(w.requests)将请求提交给各个worker。均衡服务跟踪请求待处理的数量来判断负载情况。
每个响应直接反馈给它的请求者。
你可以将循环体内的代码当成Go例程从而实现并行。
负载均衡器需要一个装很多worker的池子和一个通道来让请求者报告任务完成情况。
type Pool []*Worker type Balancer struct { pool Pool done chan *Worker }
简单!
func (b *Balancer) balance(work chan Request) { for { select { case req := <-work: // received a Request... b.dispatch(req) // ...so send it to a Worker case w := <-b.done: // a worker has finished ... b.completed(w) // ...so update its info } } }
你只需要实现dispatch和completed方法。
将负载均衡的池子用一个Heap接口实现,外加一些方法:
func (p Pool) Less(i, j int) bool { return p[i].pending < p[j].pending }
现在我们的负载均衡使用堆来跟踪负载情况。
需要的东西都有了。
// Send Request to worker func (b *Balancer) dispatch(req Request) { // Grab the least loaded worker... w := heap.Pop(&b.pool).(*Worker) // ...send it the task. w.requests <- req // One more in its work queue. w.pending++ // Put it into its place on the heap. heap.Push(&b.pool, w) }
// Job is complete; update heap func (b *Balancer) completed(w *Worker) { // One fewer in the queue. w.pending-- // Remove it from heap. heap.Remove(&b.pool, w.index) // Put it into its place on the heap. heap.Push(&b.pool, w) }
一个复杂的问题可以被拆分成容易理解的组件。
它们可以被并发的处理。
结果就是容易理解,高效,可扩展,好用。
或许更加并行。
我们有几个相同的数据库,我们想最小化延迟,分别询问他们,挑选第一个响应的。
func Query(conns []Conn, query string) Result { ch := make(chan Result, len(conns)) // buffered for _, conn := range conns { go func(c Conn) { ch <- c.DoQuery(query): }(conn) } return <-ch }
并发和垃圾回收机制让这成为一个很小很容易解决的问题。
(作业练习:处理晚来的响应。)
并发很强大。
并发不是并行。
并发帮助实现并行。
并发使并行(扩展等)变得容易。
Go: golang.org
一些历史: swtch.com/~rsc/thread/
另一个视频: tinyurl.com/newsqueak
并行不是并发(Harper): tinyurl.com/pincharper
一个并发window系统(Pike): tinyurl.com/pikecws
并发系列(McIlroy): tinyurl.com/powser
最后,并行但不是并发:
research.google.com/archive/sawzall.html