go入门教程- 版本 3 - 添加协程

  1. 版本 3 - 添加协程
  2. 19.6 用协程优化性能
    1. 链接

版本 3 - 添加协程

第 3 个版本的代码 goto_v3goto_v3

19.6 用协程优化性能

如果有太多客户端同时尝试添加 URL,第 2 个版本依旧存在性能问题。得益于锁机制,我们的 map 可以在并发访问环境下安全地更新,但每条新产生的记录都要立即写入磁盘,这种机制成为了瓶颈。写入操作可能同时发生,根据不同操作系统的特性,可能会产生数据损坏。就算不产生写入冲突,每个客户端在 Put 函数返回前,必须等待数据写入磁盘。因此,在一个 I/O 负载很高的系统中,客户端为了完成 Add 请求,将等待更长的不必要的时间。

为缓解该问题,必须对 Put 和存储进程解耦:我们将使用 Go 的并发机制。我们不再将记录直接写入磁盘,而是发送到一个通道中,它是某种形式的缓冲区,因而发送函数不必等待它完成。

保存进程会从该通道读取数据并写入磁盘。它是以 saveLoop 协程启动的独立线程。现在 mainsaveLoop 并行地执行,不会再发生阻塞。

URLStorefile 字段替换为 record 类型的通道:save chan record

1
2
3
4
5
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 通道:

1
2
3
4
5
6
7
8
9
10
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()

1
2
3
4
5
6
7
8
9
10
11
12
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 方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 章 深入学习了协程和通道,但在这里我们见到了实用的案例,更好地管理程序的不同部分。注意现在 Encoder 对象只被创建一次,而不是每次保存时都创建,这也可以节省了一些内存和运算处理。

还有一个改进可以使 goto 更灵活:我们可以将文件名、监听地址和主机名定义为标志(flag),来代替在程序中硬编码或定义常量。这样当程序启动时,可以在命令行中指定它们的新值,如果没有指定,将采用 flag 的默认值。该功能来自另一个包,所以需要 import "flag"(这个包的更详细信息见 12.4 节)。

先创建一些全局变量来保存 flag 的值:

1
2
3
4
5
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 节):

1
2
3
4
5
6
7
8
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

1
fmt.Fprintf(w, "http://%s/%s", *hostname, key)

编译或直接使用现有的可执行程序测试第 3 个版本。

链接


免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com

推荐阅读:

文章标题:go入门教程- 版本 3 - 添加协程

本文作者:知识铺

发布时间:2019-10-15, 22:30:20

最后更新:2019-10-16, 21:00:37

原始链接:https://blog.zshipu.com/2019/10/15/golang/20191015/19.6/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏