...

Codewalk: 通过通信共享内存

弹出代码
代码位置 左侧右侧 代码宽度 70% 文件路径 显示隐藏
引言
Go实现并发的方式,不同于传统的使用线程共享内存的方式。从哲学上,它可以概括为:

不要通过共享内存来通信,而要通过通信来共享内存。

信道允许你在Go程间传递数据结构的引用。如果你把它看做传递数据的所有权 (即读写该数据的能力),它们就会成为强大而富有表达能力的同步机制。

在这次的代码漫步中,我们将看到一个简单的程序。它用于轮询一个URL的列表,检查它们的 HTTP 响应代码,并定期打印出它们的状态。
doc/codewalk/urlpoll.go
State 类型
State 类型表示一个URL的状态。

Poller 会将 State 值发送到 StateMonitor,它维护了每一个URL当前状态的映射。
doc/codewalk/urlpoll.go:28,32
Resource 类型
Resource 表示URL被轮询的状态,即URL本身及其最后一次成功轮询之后遇到的错误编号。

当此程序启动时,它会为每个URL都分配一个 Resource。主Go程与 Poller Go程会在信道上互相发送 Resource。
doc/codewalk/urlpoll.go:69,73
Poller 函数
每个 Poller 都会从输入信道中接收到 Resource 的指针。在此程序中,我们约定发送者通过信道, 将底层数据的所有权传递给接收者。由此可知,不会出现两个Go程同时访问该 Resource 的情况。这就意味着我们无需担心锁会阻止对这些数据结构的并发访问。

Poller 通过调用其 Poll 方法来处理 Resource。

它会向 status 信道发送 State 值,以此将 Poll 的结果通知给 StateMonitor。

最后,它会将 Resource 的指针发送给 out 信道。这可以理解成 Poller 说:“我搞定这个 Resource 了”,然后将它的所有权返回给主Go程。

多个Go程运行多个 Poller,可以并行地处理 Resource。
doc/codewalk/urlpoll.go:99,105
Poll 方法
(Resource 类型的)Poll 方法为 Resource 的URL执行HTTP HEAD请求,并返回HTTP响应的状态码。 若有错误产生,Poll 就会将该信息记录到标准错误中,并转而返回该错误的字符串。
doc/codewalk/urlpoll.go:78,88
main 函数
main 会启动 Poller 和 StateMonitor Go程,接着经过适当的延迟后,循环地将已完成的 Resource 传回 pending 信道。
doc/codewalk/urlpoll.go:107,133
创建信道
首先,main 会创建两个 *Resource 的信道,pending 和 complete。

在 main 中,新的Go程会为每个URL发送一个 Resource 到 pending,而 main Go程则会从 complete 接收已完成的 Resource。

pending 和 complete 信道会被传至每一个 Poller Go程中,在其中,它们被称为 in 和 out。
doc/codewalk/urlpoll.go:109,110
初始化 StateMonitor
StateMonitor 会初始化并启动一个Go程,它存储了每一个 Resource 的状态。 稍后我们会看到关于此函数的细节。

现在最需要注意的,就是它会返回一个 State 的信道,该信道将作为状态保存并传至 Poller Go程。
doc/codewalk/urlpoll.go:113,114
启动 Poller Go程
现在有了必须的信道,main 会启动一些 Poller Go程,并将这些信道作为实参传入其中。 信道为 main、Poller 和 StateMonitor 提供了Go程间互相通信的手段。
doc/codewalk/urlpoll.go:117,120
将 Resource 发送至 pending
为了将初始的工作添加到此系统中,main 会启动一个新的Go程,它会为每个URL分配一个 Resource,并将其发送到 pending 中。

这个新的Go程是必要的,因为无缓存信道的发送和接收是同步的。 这也就意味着这些信道的发送操作将会阻塞,直到 Poller 对 pending 的读取操作已经就绪。

当这些在 main Go程中执行的发送与少于信道发送的 Poller 协同工作时, 该程序就会遇到死锁的情况,这是由于 main 还未从 complete 进行接收。

读者练习:修改此程序的这一部分,让它从一个文件中读取URL的列表。 (你可能想要将此Go程变为有它自己的名称的函数中。)
doc/codewalk/urlpoll.go:123,128
主事件循环
当 Poller 处理完 Resource 后,它会将该 Resource 在 complete 信道上发送。 此循环会从 complete 中接收那些 Resource 的指针。对于每一个接收到的 Resource, 它都会启动一个新的Go程调来用该 Resource 的 Sleep 方法。使用新的Go程能确保休眠并行地发生。

注意,任何单个的 Resource 指针在任何时刻都只能在 pending 或 complete 上发送。 这确保了 Resource 不是被 Poller 处理,就是休眠状态,二者不会同时发生。 这样,我们就通过通信共享了 Resource 的数据。
doc/codewalk/urlpoll.go:130,132
Sleep 方法
Sleep 在将 Resource 发送至 done 前,通过调用 time.Sleep 来暂停执行。 暂停时间可为固定的时长(pollInterval)外加一个与连续的错误次数(r.errCount)成比例的延迟。

这是个典型的Go习惯的例子:函数为了在Go程中运行,需要一个信道来发送其返回值 (或其它表示完成状态的指示)。
doc/codewalk/urlpoll.go:93,97
StateMonitor
StateMonitor 从信道中接收 State 值并周期性地输出该程序轮询的所有 Resource 的状态。
doc/codewalk/urlpoll.go:38,55
updates 信道
变量 updates 是一个 State 类型的信道,Poller Go程在其上发送 State 值。

此信道由该函数返回。
doc/codewalk/urlpoll.go:41
urlStatus 映射
变量 urlStatus 是 URL 到它们最近一次状态的映射。
doc/codewalk/urlpoll.go:42
Ticker 对象
time.Ticker 对象每隔一段指定的时间就在信道上发送一个值。

在此情况下,ticker 每隔 updateInterval 纳秒就会触发将当前的状态打印到标准输出。
doc/codewalk/urlpoll.go:43
StateMonitor Go程
StateMonitor 会一直循环,并在两个信道间进行选择:ticker.C 和 update。 select 语句会阻塞,直到其中一个通信就绪。

当 StateMonitor 从 ticker.C 接收到一次嘀嗒后,就会调用 logState 来打印当前的状态。 当它从 updates 接收到 State 的更新后,就会在 urlStatus 映射中记录新的状态。

注意,该Go程拥有 urlStatus 数据结构,以此来确保它只能被连续地访问。 在并行地读写或写入共享的映射时,这样可避免可能出现的内存数据损坏问题。
doc/codewalk/urlpoll.go:44,53
总结
在此代码漫步中,我们探索了一个简单的例子,它使用了Go的并发基原,通过通信共享了内存。

这应当为你提供了一个起点,以探索Go程和信道用法,编写富有表现力的,简洁的并发程序。
doc/codewalk/urlpoll.go