翻译进度
5
分块数量
2
参与人数

19.6. 使用 Goroutines 来提高性能

这是一篇社区协同翻译的文章,你可以点击右边区块信息里的『改进』按钮向译者提交改进建议。

如果有太多的客户端试图同时添加 URLs ,第 2 版仍然存在一个性能问题: 由于锁定机制,我们的 map 可以安全的更新并发访问,但是立即将每一个新记录写入到磁盘,是一个瓶颈。磁盘写入可能会同时发生,并且根据你的操作系统的特性,可能会导致崩溃。即使写入不会冲突,每个客户端在 Put 函数返回之前,必须等待他们的数据写入到磁盘。因此,在 I/O 负载大的系统上,客户端需要等待的时间将超过完成添加请求所必须的时间。

为了解决这个问题,我们必须将 Put 与 Save 的过程解耦合:可以通过 Go 的并发机制做到这点。我们不再将记录直接保存到磁盘,而是发送它们到一个通道,这是一种缓冲,所以发送函数不需要再等待它。

BroQiang 翻译于 6个月前

写入磁盘的保存过程从这个通道读取,并且是启动在一个叫 saveloop 的协程单独启动的线程上。主程序与 saveloop 是同时执行的,所以没有那么多的阻塞。

我们通过一个 record 类型的通道替换 URLStore 中的 file 字段: save chan record


type URLStore struct {

    urls map[string]string

    mu sync.RWMutex

    save chan record

}

一个 channel,就像一个 map 一样必须使用 make 创建;我们将修改我们的工厂 NewURLStore ,在它里面使用 make 去创建 channel ,并给他一个 1000 长度的缓冲区,如: save := make(chan record, saveQueueLength) 。为了弥补我们的性能瓶颈, Put 可以将一个 record 发送到我们的 channel 缓冲区保存,而不是进行函数调用保存每一条记录到磁盘。


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")

}
BroQiang 翻译于 6个月前

save channel 的另一端,我们必须有一个接收器: 我们的新方法 saveLoop 将运行在一个单独的 goroutine 中; 它接收 record 的值并将他们写入到一个文件。 saveLoop 也是在 NewURLStore() 函数中通过 go 关键字启动,我们现在可以删除不再需要的文件打开的代码。这里修改后的 NewURLStore() :


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 方法的代码:


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)

        }

    }

}
BroQiang 翻译于 6个月前

Records 从一个无限循环中的 save channel 读取并编码到文件。

14 章 我们深入的学习了协程与通道,但是在这里我们看到了一个有用的示例,它可以更好的管理一个程序的不同部分。还要注意,现在我们仅创建了一次 Encoder 对象, 而不是每次保存,这样也节省了一些内存和处理。

另外一个改善可以使 goto 变得更加灵活:替换代码中的 filename 、硬编码的或者作为程序中常量的监听地址和主机名,我们可以将它们定义为 flags 。

这样,当启动程序的时候,如果在命令行中输入这些值,它们将被替换成新的值,如果没有输入,将从 flag 中获取默认值。这个功能来自一个不同的包,所以我们必须: import "flag" (关于此包的详细信息,参见 章节 12.4 )。

我们首先创建一些全局变量去保存 flag 的值:


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")

)
BroQiang 翻译于 6个月前

为了处理命令行参数,我们必须添加 flag.Parse() 到 main 函数中,并且在 flags 被解析后实例化 URLStore ,因为参数解析后我们才能知道 dataFile 的值( 在代码中使用的是 *dataFile ,这时因为 flag 是一个指针,必须取消引用去获取值,参见 章节 4.9 ):


var store *URLStore

func main() {

    flag.Parse()

    store = NewURLStore(*dataFile)

    http.HandleFunc("/", Redirect)

    http.HandleFunc("/add", Add)

    http.ListenAndServe(*listenAddr, nil)

}

在 Add 处理器中,我们现在必须将 localhost:8080 替换成 *hostname


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

编译并测试第 3 版本或使用目录中的可执行文件(译者注:一样不用去纠结为什么没有这个文件,直接编译去执行就好了)。

版本 4 —— 使用 json 做持久存储

第 4 版 goto_4 的代码(在 章节 19.7 中讨论 )能在 code_examples\chapter_19\goto_v4. 中找到。

BroQiang 翻译于 6个月前

本文章首发在 GolangCaff
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

参与译者:2
讨论数量: 0
发起讨论


暂无话题~