这是一个使用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