Files
the-way-to-go_ZH_CN/eBook/19.6.md
marjune dce192535f Add chapter 19.6 19.7 (#707)
* add chapter 19.6

* add chapter 19.7
2019-08-05 18:18:24 -07:00

110 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 版本 3 - 添加协程
第 3 个版本的代码 *goto_v3* 见 [goto_v3](examples/chapter_19/goto_v3)。
# 19.6 用协程优化性能
如果有太多客户端同时尝试添加 URL第 2 个版本依旧存在性能问题。得益于锁机制,我们的 map 可以在并发访问环境下安全地更新,但每条新产生的记录都要立即写入磁盘,这种机制成为了瓶颈。写入操作可能同时发生,根据不同操作系统的特性,可能会产生数据损坏。就算不产生写入冲突,每个客户端在 `Put` 函数返回前,必须等待数据写入磁盘。因此,在一个 I/O 负载很高的系统中,客户端为了完成 `Add` 请求,将等待更长的不必要的时间。
为缓解该问题,必须对 `Put` 和存储进程*解耦*:我们将使用 Go 的并发机制。我们不再将记录直接写入磁盘,而是发送到一个*通道*中,它是某种形式的缓冲区,因而发送函数不必等待它完成。
保存进程会从该通道读取数据并写入磁盘。它是以 `saveLoop` 协程启动的独立线程。现在 `main``saveLoop` 并行地执行,不会再发生阻塞。
`URLStore``file` 字段替换为 `record` 类型的通道:`save chan record`
```go
type URLStore struct {
urls map[string]string
mu sync.RWMutex
save chan record
}
```
通道和 map 一样,必须用 `make` 创建。我们会以此修改 `NewURLStore` 工厂函数并给定缓冲区大小为1000例如`save := make(chan record, saveQueueLength)`。为解决性能问题,`Put` 可以发送记录 record 到带缓冲的 `save` 通道:
```go
func (s *URLStore) Put(url string) string {
for {
key := genKey(s.Count())
if s.Set(key, url) {
s.save <- record{key, url}
return key
}
}
panic("shouldn't get here")
}
```
`save` 通道的另一端必须有一个接收者:新的 `saveLoop` 方法在独立的协程中运行,它接收 record 值并将它们写入到文件。`saveLoop` 是在 `NewURLStore()` 函数中用 `go` 关键字启动的。现在,可以移除不必要的打开文件的代码。以下是修改后的 `NewURLStore()`
```go
const saveQueueLength = 1000
func NewURLStore(filename string) *URLStore {
s := &URLStore{
urls: make(map[string]string),
save: make(chan record, saveQueueLength),
}
if err := s.load(filename); err != nil {
log.Println("Error loading URLStore:", err)
}
go s.saveLoop(filename)
return s
}
```
以下是 `saveLoop` 方法的代码:
```go
func (s *URLStore) saveLoop(filename string) {
f, err := os.Open(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal("URLStore:", err)
}
defer f.Close()
e := gob.NewEncoder(f)
for {
// taking a record from the channel and encoding it
r := <-s.save
if err := e.Encode(r); err != nil {
log.Println("URLStore:", err)
}
}
}
```
在无限循环中,记录从 `save` 通道读取,然后编码到文件中。
我们在 [14 章](14.0.md) 深入学习了协程和通道,但在这里我们见到了实用的案例,更好地管理程序的不同部分。注意现在 `Encoder` 对象只被创建一次,而不是每次保存时都创建,这也可以节省了一些内存和运算处理。
还有一个改进可以使 goto 更灵活我们可以将文件名、监听地址和主机名定义为标志flag来代替在程序中硬编码或定义常量。这样当程序启动时可以在命令行中指定它们的新值如果没有指定将采用 flag 的默认值。该功能来自另一个包,所以需要 `import "flag"`(这个包的更详细信息见 [12.4 节](12.4.md))。
先创建一些全局变量来保存 flag 的值:
```go
var (
listenAddr = flag.String("http", ":8080", "http listen address")
dataFile = flag.String("file", "store.gob", "data store file name")
hostname = flag.String("host", "localhost:8080", "host name and port")
)
```
为了处理命令行参数,必须把 `flag.Parse()` 添加到 `main` 函数中,在 flag 解析后才能实例化 `URLStore`,一旦得知了 `dataFile` 的值(在代码中使用了 `*dataFile`,因为 flag 是指针类型必须解除引用来获取值,见 [4.9 节](04.9.md)
```go
var store *URLStore
func main() {
flag.Parse()
store = NewURLStore(*dataFile)
http.HandleFunc("/", Redirect)
http.HandleFunc("/add", Add)
http.ListenAndServe(*listenAddr, nil)
}
```
现在 `Add` 处理函数中须用 `*hostname` 替换 `localhost:8080`
```go
fmt.Fprintf(w, "http://%s/%s", *hostname, key)
```
编译或直接使用现有的可执行程序测试第 3 个版本。
## 链接
- [目录](directory.md)
- 上一节:[持久化存储gob](19.5.md)
- 下一节:[以 json 格式存储](19.7.md)