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