go 并发的非阻塞缓存

2023-10-27

本节中我们会做一个无阻塞的缓存,这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存(memoizing)函数(译注:Memoization的定义: memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing.),也就是说,我们需要缓存函数的返回结果,这样在对函数进行调用的时候,我们就只需要一次计算,之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。

我们将使用下面的httpGetBody函数作为我们需要缓存的函数的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比较大的,所以我们尽量避免在不必要的时候反复调用。

func httpGetBody(url string) (interface{}, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return ioutil.ReadAll(resp.Body)
}

最后一行稍微隐藏了一些细节。ReadAll会返回两个结果,一个[]byte数组和一个错误,不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error类型,所以我们也就可以这样返回结果并且不需要额外的工作了。我们在httpGetBody中选用这种返回类型是为了使其可以与缓存匹配。

下面是我们要设计的cache的第一个“草稿”:

gopl.io/ch9/memo1

// Package memo provides a concurrency-unsafe
// memoization of a function of type Func.
package memo

// A Memo caches the results of calling a Func.
type Memo struct {
    f     Func
    cache map[string]result
}

// Func is the type of the function to memoize.
type Func func(key string) (interface{}, error)

type result struct {
    value interface{}
    err   error
}

func New(f Func) *Memo {
    return &Memo{f: f, cache: make(map[string]result)}
}

// NOTE: not concurrency-safe!
func (memo *Memo) Get(key string) (interface{}, error) {
    res, ok := memo.cache[key]
    if !ok {
        res.value, res.err = memo.f(key)
        memo.cache[key] = res
    }
    return res.value, res.err
}

Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是简单的函数返回的值对儿--一个值和一个错误值。继续下去我们会展示一些Memo的变种,不过所有的例子都会遵循上面的这些方面。

下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get,并打印调用延时以及其返回的数据大小的log:

m := memo.New(httpGetBody)
for url := range incomingURLs() {
    start := time.Now()
    value, err := m.Get(url)
    if err != nil {
        log.Print(err)
    }
    fmt.Printf("%s, %s, %d bytes\n",
    url, time.Since(start), len(value.([]byte)))
}

我们可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。从下面的测试输出,我们可以看到URL流包含了一些重复的情况,尽管我们第一次对每一个URL的(*Memo).Get的调用都会花上几百毫秒,但第二次就只需要花1毫秒就可以返回完整的数据了。

$ go test -v gopl.io/ch9/memo1
=== RUN   Test
https://golang.org, 175.026418ms, 7537 bytes
https://godoc.org, 172.686825ms, 6878 bytes
https://play.golang.org, 115.762377ms, 5767 bytes
http://gopl.io, 749.887242ms, 2856 bytes
https://golang.org, 721ns, 7537 bytes
https://godoc.org, 152ns, 6878 bytes
https://play.golang.org, 205ns, 5767 bytes
http://gopl.io, 326ns, 2856 bytes
--- PASS: Test (1.21s)
PASS
ok  gopl.io/ch9/memo1   1.257s

这个测试是顺序地去做所有的调用的。

由于这种彼此独立的HTTP请求可以很好地并发,我们可以把这个测试改成并发形式。可以使用sync.WaitGroup来等待所有的请求都完成之后再返回。

m := memo.New(httpGetBody)
var n sync.WaitGroup
for url := range incomingURLs() {
    n.Add(1)
    go func(url string) {
        start := time.Now()
        value, err := m.Get(url)
        if err != nil {
            log.Print(err)
        }
        fmt.Printf("%s, %s, %d bytes\n",
        url, time.Since(start), len(value.([]byte)))
        n.Done()
    }(url)
}
n.Wait()

这次测试跑起来更快了,然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss(缓存未命中),或者命中了缓存但却返回了错误的值,或者甚至会直接崩溃。

但更糟糕的是,有时候这个程序还是能正确的运行(译:也就是最让人崩溃的偶发bug),所以我们甚至可能都不会意识到这个程序有bug。但是我们可以使用-race这个flag来运行程序,竞争检测器(§9.6)会打印像下面这样的报告:

$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1
=== RUN   TestConcurrent
...
WARNING: DATA RACE
Write by goroutine 36:
  runtime.mapassign1()
      ~/go/src/runtime/hashmap.go:411 +0x0
  gopl.io/ch9/memo1.(*Memo).Get()
      ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
  ...
Previous write by goroutine 35:
  runtime.mapassign1()
      ~/go/src/runtime/hashmap.go:411 +0x0
  gopl.io/ch9/memo1.(*Memo).Get()
      ~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
...
Found 1 data race(s)
FAIL    gopl.io/ch9/memo1   2.393s

memo.go的32行出现了两次,说明有两个goroutine在没有同步干预的情况下更新了cache map。这表明Get不是并发安全的,存在数据竞争。

28  func (memo *Memo) Get(key string) (interface{}, error) {
29      res, ok := memo.cache(key)
30      if !ok {
31          res.value, res.err = memo.f(key)
32          memo.cache[key] = res
33      }
34      return res.value, res.err
35  }

最简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一个mutex,在Get的一开始获取互斥锁,return的时候释放锁,就可以让cache的操作发生在临界区内了:

gopl.io/ch9/memo2

type Memo struct {
    f     Func
    mu    sync.Mutex // guards cache
    cache map[string]result
}

// Get is concurrency-safe.
func (memo *Memo) Get(key string) (value interface{}, err error) {
    memo.mu.Lock()
    res, ok := memo.cache[key]
    if !ok {
        res.value, res.err = memo.f(key)
        memo.cache[key] = res
    }
    memo.mu.Unlock()
    return res.value, res.err
}

测试依然并发进行,但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我们完全丧失了并发的性能优点。每次对f的调用期间都会持有锁,Get将本来可以并行运行的I/O操作串行化了。我们本章的目的是完成一个无锁缓存,而不是现在这样的将所有请求串行化的函数的缓存。

下一个Get的实现,调用Get的goroutine会两次获取锁:查找阶段获取一次,如果查找没有返回任何内容,那么进入更新阶段会再次获取。在这两次获取锁的中间阶段,其它goroutine可以随意使用cache。

gopl.io/ch9/memo3

func (memo *Memo) Get(key string) (value interface{}, err error) {
    memo.mu.Lock()
    res, ok := memo.cache[key]
    memo.mu.Unlock()
    if !ok {
        res.value, res.err = memo.f(key)

        // Between the two critical sections, several goroutines
        // may race to compute f(key) and update the map.
        memo.mu.Lock()
        memo.cache[key] = res
        memo.mu.Unlock()
    }
    return res.value, res.err
}

这些修改使性能再次得到了提升,但有一些URL被获取了两次。这种情况在两个以上的goroutine同一时刻调用Get来请求同样的URL时会发生。多个goroutine一起查询cache,发现没有值,然后一起调用f这个慢不拉叽的函数。在得到结果后,也都会去更新map。其中一个获得的结果会覆盖掉另一个的结果。

理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个条目包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在条目的结果被设置之后,这个channel就会被关闭,以向其它goroutine广播(§8.9)去读取该条目内的结果是安全的了。

gopl.io/ch9/memo4

type entry struct {
    res   result
    ready chan struct{} // closed when res is ready
}

func New(f Func) *Memo {
    return &Memo{f: f, cache: make(map[string]*entry)}
}

type Memo struct {
    f     Func
    mu    sync.Mutex // guards cache
    cache map[string]*entry
}

func (memo *Memo) Get(key string) (value interface{}, err error) {
    memo.mu.Lock()
    e := memo.cache[key]
    if e == nil {
        // This is the first request for this key.
        // This goroutine becomes responsible for computing
        // the value and broadcasting the ready condition.
        e = &entry{ready: make(chan struct{})}
        memo.cache[key] = e
        memo.mu.Unlock()

        e.res.value, e.res.err = memo.f(key)

        close(e.ready) // broadcast ready condition
    } else {
        // This is a repeat request for this key.
        memo.mu.Unlock()

        <-e.ready // wait for ready condition
    }
    return e.res.value, e.res.err
}

现在Get函数包括下面这些步骤了:获取互斥锁来保护共享变量cache map,查询map中是否存在指定条目,如果没有找到那么分配空间插入一个新条目,释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)时,goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话,可以直接从ready channel中读取,由于这个读取操作在channel关闭之前一直是阻塞。

如果没有条目的话,需要向map中插入一个没有准备好的条目,当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。

条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值,其它goroutine在收到"ready"的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问,但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前,因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。

这样并发、不重复、无阻塞的cache就完成了。

上面这样Memo的实现使用了一个互斥量来保护多个goroutine调用Get时的共享map变量。不妨把这种设计和前面提到的把map变量限制在一个单独的monitor goroutine的方案做一些对比,后者在调用Get时需要发消息。

Func、result和entry的声明和之前保持一致:

// Func is the type of the function to memoize.
type Func func(key string) (interface{}, error)

// A result is the result of calling a Func.
type result struct {
    value interface{}
    err   error
}

type entry struct {
    res   result
    ready chan struct{} // closed when res is ready
}

然而Memo类型现在包含了一个叫做requests的channel,Get的调用方用这个channel来和monitor goroutine来通信。requests channel中的元素类型是request。Get的调用方会把这个结构中的两组key都填充好,实际上用这两个变量来对函数进行缓存的。另一个叫response的channel会被拿来发送响应结果。这个channel只会传回一个单独的值。

gopl.io/ch9/memo5

// A request is a message requesting that the Func be applied to key.
type request struct {
    key      string
    response chan<- result // the client wants a single result
}

type Memo struct{ requests chan request }
// New returns a memoization of f.  Clients must subsequently call Close.
func New(f Func) *Memo {
    memo := &Memo{requests: make(chan request)}
    go memo.server(f)
    return memo
}

func (memo *Memo) Get(key string) (interface{}, error) {
    response := make(chan result)
    memo.requests <- request{key, response}
    res := <-response
    return res.value, res.err
}

func (memo *Memo) Close() { close(memo.requests) }

上面的Get方法,会创建一个response channel,把它放进request结构中,然后发送给monitor goroutine,然后马上又会接收它。

cache变量被限制在了monitor goroutine `(*Memo).server中,下面会看到。monitor会在循环中一直读取请求,直到request channel被Close方法关闭。每一个请求都会去查询cache,如果没有找到条目的话,那么就会创建/插入一个新的条目。

func (memo *Memo) server(f Func) {
    cache := make(map[string]*entry)
    for req := range memo.requests {
        e := cache[req.key]
        if e == nil {
            // This is the first request for this key.
            e = &entry{ready: make(chan struct{})}
            cache[req.key] = e
            go e.call(f, req.key) // call f(key)
        }
        go e.deliver(req.response)
    }
}

func (e *entry) call(f Func, key string) {
    // Evaluate the function.
    e.res.value, e.res.err = f(key)
    // Broadcast the ready condition.
    close(e.ready)
}

func (e *entry) deliver(response chan<- result) {
    // Wait for the ready condition.
    <-e.ready
    // Send the result to the client.
    response <- e.res
}

和基于互斥量的版本类似,第一个对某个key的请求需要负责去调用函数f并传入这个key,将结果存在条目里,并关闭ready channel来广播条目的ready消息。使用(*entry).call来完成上述工作。

紧接着对同一个key的请求会发现map中已经有了存在的条目,然后会等待结果变为ready,并将结果从response发送给客户端的goroutien。上述工作是用(*entry).deliver来完成的。对call和deliver方法的调用必须让它们在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。

这个例子说明我们无论用上锁,还是通信来建立并发程序都是可行的。

上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注:不是说好的golang推崇通信并发么)

练习 9.3: 扩展Func类型和(*Memo).Get方法,支持调用方提供一个可选的done channel,使其具备通过该channel来取消整个操作的能力(§8.9)。一个被取消了的Func的调用结果不应该被缓存。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

go 并发的非阻塞缓存 的相关文章

  • 打印到 stdout 会导致阻塞的 goroutine 运行吗?

    作为一个愚蠢的基本线程练习 我一直在尝试实现理发师睡觉的问题 http en wikipedia org wiki Sleeping barber problem在戈兰 对于通道来说 这应该很容易 但我遇到了一个 heisenbug 也就是
  • 如何使信号量超时

    Go 中的信号量是通过通道来实现的 一个例子是这样的 https sites google com site gopatterns concurrency semaphores https sites google com site gop
  • Golang中按长度分割字符串

    有谁知道如何在 Golang 中按长度分割字符串 例如 每 3 个字符分割 helloworld 那么理想情况下它应该返回一个 hel low orl d 数组 或者 一个可能的解决方案是在每 3 个字符后附加一个换行符 所有的想法都非常感
  • 为什么 json.Unmarshal 返回映射而不是预期的结构?

    看看这个游乐场 http play golang org p dWku6SPqj5 http play golang org p dWku6SPqj5 基本上 我正在工作的图书馆收到了interface 作为参数 然后需要json Unma
  • Golang 正则表达式在字符串之间替换

    我有一些可能采用以下形式的字符串 MYSTRING MYSTRING n MYSTRING n MYSTRING randomstringwithvariablelength n 我希望能够将其正则表达式为MYSTRING foo 基本上替
  • 直接从一个通道发送到另一个通道

    当从一个通道直接发送到另一个通道时 我偶然发现了令人惊讶的行为 package main import fmt func main my chan make chan string chan of chans make chan chan
  • os.Mkdir 和 os.MkdirAll 权限

    我正在尝试在程序开始时创建一个日志文件 我需要检查是否 log如果不创建目录 则目录存在 然后继续创建日志文件 好吧 我尝试使用os Mkdir 也os MkdirAll 但无论我在第二个参数中输入什么值 我都会得到一个没有权限的锁定文件夹
  • 如何同时使用 LoadHTMLGlob 和 LoadHTMLFiles

    我想要来自不同子目录的分隔符逻辑模板templates文件夹 下面是我的templates文件夹 templates authentication login gohtml logout gohtml index gohtml profil
  • 如何使用 GOPATH 的 Samba 服务器位置?

    我正在尝试将 GOPATH 设置为共享网络文件夹 当我进入 export GOPATH smb path to shared folder I get go GOPATH entry is relative must be absolute
  • 如何修复“缺少表的 FROM 子句条目”错误

    我正在尝试根据游戏 ID 获取平台名称 我有如下三个表 我正在尝试加入它们以获得所需的结果 Games Id 1 2 3 4 Game Platforms Id game id platform id 1 1 1 2 1 2 3 3 3
  • 在 Go to 函数中通过引用和值传递

    我对 Go 中通过引用和值传递有点困惑 我已经看到过对类型前面的 的解释 在类型名称前面 表示声明的变量将存储该类型的另一个变量的地址 而不是该类型的值 类型 这对我来说毫无意义 在Java中 如果我将数据库实例传递给函数 我会这样做 da
  • 什么时候返回结构体指针是个好主意?

    我正在学习 Go 我对何时使用指针有点困惑 具体来说 当返回一个struct从函数中 什么时候适合返回结构体实例本身 什么时候适合返回指向结构体的指针 示例代码 type Car struct make string model strin
  • 如何在 Go 中填写 void* C 指针?

    我正在尝试与 Go 中的一些 C 代码交互 使用 cgo 这一直相对简单 直到我遇到这种 相当常见 的情况 需要将指针传递给本身包含指向某些数据的指针的结构 我似乎无法弄清楚如何从 Go 中做到这一点 而不诉诸于将结构的创建放入 C 代码本
  • 有没有办法间歇性地执行重复性任务?

    有没有办法在 Go 中执行重复的后台任务 我在想类似的事情Timer schedule task delay period 在爪哇 我知道我可以用 goroutine 来做到这一点Time sleep 但我想要一些容易停止的东西 这是我得到
  • 我们如何在 Go 中使用通道来代替互斥锁?

    通道将通信 值的交换 与同步相结合 保证两个计算 goroutine 处于已知状态 如何使用 Google Go 中的通道来执行互斥量的功能 package main import sync var global int 0 var m s
  • 为什么 DER ASN.1 大整数的解组在 Golang 中仅限于 SEQUENCE?

    我希望能够使用encoding asn1 包从 DER 文件中解组一个大整数 但它看起来只适用于整数序列 例如 这不起作用 这很奇怪 因为 Big Int 的编组效果很好 https play golang org p Wkj0jAA6bp
  • go json marshal 的默认大小写选项?

    我有以下结构要导出为 json type ExportedIncident struct Title string json title Host string json host Status string json status Dat
  • 如何使用 go1.6.2 构建 linux 32 位

    有没有任何组合GOARCH and GOOS我可以设置哪些值来构建 ELF 32 位二进制文 件 GOOS linux and GOARCH 386 更多示例 架构 32 bit gt GOARCH 386 64 bit gt GOARCH
  • 如何通过在切片上查找来从切片复制到数组

    我正在编写一个库来处理二进制格式 我有一个带有数组变量的结构 我想保留它以用于文档目的 我还需要从输入字节片中查找和判断 一些伪代码 type foo struct boo 5 byte coo 3 byte func main input
  • 如何在golang中获得两个切片的交集?

    Go 中有没有有效的方法来获取两个切片的交集 我想避免嵌套 for 循环之类的解决方案slice1 string foo bar hello slice2 string foo bar intersection slice1 slice2

随机推荐

  • IDEA配置运行非Maven项目-非常详细

    文章目录 一 写在前面 1 1 为啥会有此篇博文 1 2 准备 二 正文 2 1 使用IDEA打开 Open 项目 2 2 打开项目结构 Project Structure 2 2 1 打开方式 2 2 2 项目结构配置内容总览 2 2 3
  • DANA简介

    Dynamically Allocated Neural Network DANA Accelerator https github com bu icsg dana spinalHDL Doc https spinalhdl github
  • 04-----git查看相应信息的操作

    git branch a 查看所有分支 git log p master remotes origin master 查看本地的master分支和remotes origin master分支的差别 git status 可以查看到工作区
  • shell 脚本学习

    1 bin bash 指定解释器是 bash 解释器有几种 2 a 1 给a赋值为1 注意中间不能有空格 赋值时不能有空格 3 unset a 取消变量a 4 readonly a 10 声明a为只读变量 并且赋值为10 注意这时就不能用u
  • Flutter环境搭建报错:What went wrong: A problem occurred configuring root project 'android'

    window下 在AS中搭建Flutter环境 创建Flutter项目报错 报错信息如下 文件加载失败 需要更换gradle文件中的镜像 找到 gradle文件 2 更改镜像 更改镜像 repositories google jcenter
  • 软件2.0时代之三:毛新生谈他眼中的Web2.0

    软件2 0时代之三 毛新生谈他眼中的Web2 0 2007 08 31 来自 CSDN 共有评论 7 条 发表评论 收藏到我的网摘 Web是一个 活物 是一个时时新 日日新 每天都在生长着的世界上最大的开放分布式系统 Web和以前的IT系统
  • How to Install one Plug-in into Eclipse

    How to Install One New Plug in Old Way before Eclipse 3 4 Installed new plug ins by dumping them in the eclipse plugins
  • 笔试

    文章目录 前言 36 异步FIFO的设计 1 先从同步FIFO说起 2 异步FIFO介绍 3 空满判断 4 跨时钟域问题 5 关于格雷码的转换 6 代码实现异步FIFO 7 几点思考 8 写在后面 往期精彩 前言 嗨 来啦 今天学习一个 比
  • 对象数组去重——数组删除所有含有固定id的对象

    一 重要方法 filter 方法可以创建一个新的数组 新数组中的元素是通过检查指定数组中符合条件的所有元素 1 数组去重 86 77 77 86 gt 86 77 去除重复元素依靠的是indexOf总是返回第一个元素的位置 后续的重复元素位
  • The inferior stopped because it received a signal from the Operating System.

    前景提要 要理解这个错误的根源 根源 用户的指针指向了系统的内存区域 表象 程序异常结束 exe crashed 编译可以通过 dedug时出现 The inferior stopped because it received a sign
  • 阿里云 OSS介绍

    1 什么是阿里云 OSS OSS 为 Object Storage Service 即对象存储服务 是阿里云提供的海量 安全 低成本 高可靠的云存储服务 OSS 具有与平台无关的 RESTful API 接口 可以在任意应用 任意时间 任意
  • 代码重复率检查工具jsinspect 检查重复代码,去掉冗余代码。

    jscpd jsinspect npm install g jsinspect 检测复制粘贴和结构类似的JavaScript代码
  • Flutter 图片裁剪

    参考一 参考二 参考三
  • 「OceanBase 4.1 体验」|大厂开始接入的国产分布式数据库,不来了解了解?

    OceanBase 4 1 体验 前言 OCP Express在线升级功能 租户级物理备库 TP 事务处理 和AP 分析处理 优化 TP 性能优化 AP 性能优化 结尾 前言 上次我们讲了本人自己亲自上手OceanBase 4 1的初体验
  • 自动化测试实现多线程

    自动化测试实现多线程 进程 进程就是一个程序在一个数据集上的一次动态执行过程 我们编写的程序用来描述进程要完成哪些功能以及如何完成 线程 线程页脚轻量级进程 他是一个基本的CPU执行单元 是进程中的实现 线程的出现是为了降低上下文切换的小号
  • vue调试工具vue-devtools安装及使用(支持vue3版本)

    github下载地址 https gitee com h5web devtools 6 0 0 beta 15 1 下载Github源文件 devtools 6 0 0 beta 15 git clone https gitee com h
  • Blender雕刻基础:使用方法与技巧

    如何正确进入雕刻模式 1 在启动对话框界面中或是文件菜单新建项 选择Sculpting直接进入雕刻模式 选择雕刻模式默认只有Sculpting 雕刻 和Shading 着色 两个选项卡 2 如果选择的是常规 界面中的物体并不是真正用以雕刻的
  • activiti源码解析系列8 - 任务完成命令类

    我们在完成任务的时候都执行了哪些操作呢 主要涉及删除表 默认非级联 ACT RU TASK ACT RU IDENTITYLINK ACT RU VARIABLE 主要看一个CompleteTaskCmd protected Void ex
  • react,tsx中使用微信jssdk分享总结

    React tsx的H5项目使用企业微信JS SDK 步骤 1 配置域名 点击企业微信PC版左下角登录管理后台 应用管理 应用 设置应用主页 网页授权及JS SDK 2 引入JS SDK 在React项目内终端下载 npm install
  • go 并发的非阻塞缓存

    本节中我们会做一个无阻塞的缓存 这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题 这个问题叫作缓存 memoizing 函数 译注 Memoization的定义 memoization 一词是Donald Mic