标签 Package 下的文章

没有VPS搭建govanityurls服务?别急!你依然可以自定义Go包导入路径

img{512x368}

我们见到的Go包的导入路径常常以github.com、bitbucket.org等代码托管站点的域名为前缀,这样的包导入路径有一个问题,那就是当Go包的托管站点发生变更时(比如从github.om迁移到bitbucket.org或gitlab.com),该包的使用者需要更新包的导入路径。当然,在支持go module+GOPROXY的情况下,如果使用者不再升级包版本,他/她完全可以继续使用原包导入路径,但这仅是特例。

还有一些包的导入路径并非以知名代码托管站点域名作为前缀,比如:Go官方扩展包text,它的包导入路径是golang.org/x/text,这种包导入路径被称为vanity import path,字面义是虚荣心导入路径,即以个人或组织官方域名作为前缀的包导入路径。采用vanity import path的包避免了包迁移对包使用者的影响。包使用者完全无需关心包的实际存储位置是在github上还是在bitbucket上或是私有服务器上。同时将vanity import path作为包的权威路径(canonical import path),也方便go get等对包权威路径的检查,避免包路径变更的前后不一致。

之前笔者曾经写过两篇文章介绍了利用govanityurls这个工具实现自定义包导入路径的方法。不过这种方法有一个约束条件,那就是你需要有一台VPS主机来部署运行govanityurls。虽然现在的云主机很便宜,但是购买和自建毕竟还是要付出一定成本的。如果没有VPS搭建govanityurls服务,那我们是否还有其他方法来自定义Go包导入路径呢?答案当然是

根据Go官方关于go get命令的文档,当go get从非知名托管站点(github, bitbucket等之外的站点)获取go包时,会尝试在返回的http/https应答head标签中查找是否有如下形式meta标签:

<meta name="go-import" content="import-prefix vcs repo-root">

meta标签中的name值是固定的”go-import”,import-prefix即包vanity import path,比如:go.tonybai.com/gocmpp;vcs是采用的版本控制工具,git、svn或hg等;repo-root是包代码的实际存储服务器url。

下面是一个实际例子:

<meta name="go-import" content="go.tonybai.com/gocmpp git https://github.com/bigwhite/gocmpp">

对于这样的标签,go get会做进一步匹配(可参见GOROOT/src/cmd/go/internal/get/vcs.go中的matchGoImport函数实现),看content值中的import-prefix是否是go get所需要的包的导入路径。如果是,则会向真正存储包代码的服务器再次发起代码获取请求(比如:git clone等)。

你可能会说:我用一个静态站点服务也能返回这样的应答。没错!但搭建静态站点一般还是需要VPS,这里我们介绍一种无须VPS的方法:利用github pages

下面是利用github pages实现自定义Go包导入路径的原理图:

img{512x368}

图:利用github pages实现自定义Go包导入路径

下面我们就以go.tonybai.com/gocmpp这个包导入路径的定制步骤来说明一下上述原理。

首先,我们要给tonybai.com这个域名添加一个子域名:go.tonybai.com作为我个人生产的所有Go包的导入路径前缀。我在DNS设置中为go.tonybai.com指定一个CNAME值:go.tonybai.com.github.io。这样当访问go.tonybai.com时,实际上是向go.tonybai.com.github.io发起请求。当然此刻如果你向go.tonybai.com发起请求时,你必然会得到404错误,因为github尚未建立起go.tonybai.com.github.io这个站点。

接下来,我们就来建立go.tonybai.com.github.io这个基于github pages的静态站点。我创建一个新的代码仓库:github.com/bigwhite/go.tonybai.com.github.io,在该仓库的”Settings”标签中,我们启用github pages,并将该仓库的master分支作为站点的根路径。在同一页面的Custom domain下,我们填入go.tonybai.com,点击save保存。github会在该仓库中创建一个名为CNAME的文件,其内容如下:

$cat CNAME

go.tonybai.com

表示该站点绑定了自定义域名:go.tonybai.com

正常情况下,你还可以在Settings标签下启用该静态站点的HTTPS服务,github会自动向Let’s Encrypt发起证书申请。

注:由于我的域名之前已经在Let’s Encrypt申请过相关证书,这里始终失败。这样导致后续我们只能使用go get -insecure去获取Go包代码。

在该仓库中,我们创建一个名为gocmpp的文件:

<html>
    <head>
        <meta name="go-import" content="go.tonybai.com/gocmpp git https://github.com/bigwhite/gocmpp">
        <meta http-equiv="refresh" content="0;URL='https://github.com/bigwhite/gocmpp'">
    </head>
    <body>
        Redirecting you to the <a href="https://github.com/bigwhite/gocmpp">project page</a>...
    </body>
</html>

该文件内容作为访问go.tonybai.com/gocmpp的请求的应答。

大约20分钟后,github pages内容生效。我们就可以使用下面命令去获取本存储在github.com/bigwhite/gocmpp下面的包了:

$go get go.tonybai.com/gocmpp

由于证书问题,这里我们只能用go get -insecure,即让go get使用http协议发起请求。

在gopath mode下,我们的执行结果如下:



$GO111MODULE=off go get -x -v -insecure go.tonybai.com/gocmpp
# get https://go.tonybai.com/gocmpp?go-get=1
# get https://go.tonybai.com/gocmpp?go-get=1: 200 OK (1.012s)
get "go.tonybai.com/gocmpp": found meta tag get.metaImport{Prefix:"go.tonybai.com/gocmpp", VCS:"git", RepoRoot:"https://github.com/bigwhite/gocmpp"} at //go.tonybai.com/gocmpp?go-get=1
go.tonybai.com/gocmpp (download)
cd .
git clone -- https://github.com/bigwhite/gocmpp /Users/tonybai/Go/src/go.tonybai.com/gocmpp
cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp
git submodule update --init --recursive
cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp
git show-ref
cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp
git submodule update --init --recursive

.... ....

cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp
/Users/tonybai/.bin/go1.14/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p go.tonybai.com/gocmpp -complete -buildid O9VmohLTciBDjallbacN/O9VmohLTciBDjallbacN -goversion go1.14 -D "" -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go
/Users/tonybai/.bin/go1.14/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/ec/ec99b1c49c84d1e2edf88bee646f17198acc38c2c8f5a3d859540a394d6c5d0c-d # internal
mkdir -p /Users/tonybai/Go/pkg/darwin_amd64/go.tonybai.com/
mv $WORK/b001/_pkg_.a /Users/tonybai/Go/pkg/darwin_amd64/go.tonybai.com/gocmpp.a
rm -r $WORK/b001/
/Users/tonybai/go/src git:(master)  $tree -L 1 go.tonybai.com

go.tonybai.com
└── gocmpp

1 directory, 0 files

我们看到go get成功通过go.tonybai.com/gocmpp获取到gocmpp包,并编译安装成功(安装到GOPATH/pkg/下面)。

下面是module-aware模式下的go get获取结果:

$GOPROXY='direct' go get -insecure -x -v go.tonybai.com/gocmpp

# get https://go.tonybai.com/?go-get=1
# get https://go.tonybai.com/gocmpp?go-get=1
# get https://go.tonybai.com/?go-get=1: 200 OK (1.032s)
# get https://go.tonybai.com/gocmpp?go-get=1: 200 OK (1.056s)
get "go.tonybai.com/gocmpp": found meta tag get.metaImport{Prefix:"go.tonybai.com/gocmpp", VCS:"git", RepoRoot:"https://github.com/bigwhite/gocmpp"} at //go.tonybai.com/gocmpp?go-get=1
mkdir -p /Users/tonybai/Go/pkg/mod/cache/vcs # git3 https://github.com/bigwhite/gocmpp
... ...
0.017s # cd /Users/tonybai/Go/pkg/mod/cache/vcs/63c8ecfc5ed2c830894c13fd15ab1494ce9897aefba1d11c78740b046033e9ae; git cat-file blob 0f5a658fda5e029943f9b256fefe4fa4550e7906:go.mod
go get: go.tonybai.com/gocmpp@v0.0.0-20200715060927-0f5a658fda5e: parsing go.mod:
    module declares its path as: github.com/bigwhite/gocmpp
            but was required as: go.tonybai.com/gocmpp

我们看到go get同样获取到了gocmpp module,但是由于module-aware模式下,go get会对module根路径进行检查,因此go get发现了go.mod中的module根路径:github.com/bigwhite/gocmpp与要获取的module路径(go.tonybai.com/gocmpp)不符并报错。我们更新一下gocmpp项目中的go.mod内容后,这个问题将不复存在。

这样,我们在没有VPS的前提下也实现了自定义包导入路径。后续每当我创建一个新module或新包,我只需向该仓库(go.tonybai.com.github.io)提交一个以module或package名字命名的文件即可,就像上的gocmpp文件那样。

* 参考资料:https://gianarb.it/blog/go-mod-vanity-url

我的Go技术专栏:“改善Go语⾔编程质量的50个有效实践”上线了,欢迎大家订阅学习!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线>了,感谢小伙伴们学习支持!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接>口丰富,支持长短信,签名可选。

2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily(Go每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

通过实例深入理解sync.Map的工作原理

img{512x368}

注:本文首发于笔者的个人微信公众号”iamtonybai”,是公号付费文章(价格1元)。首发于2020.10.9日,经过一个月收费期,我觉得将其免费分享出来。如果你觉得文章质量不错,欢迎到首发地址付费支持:https://mp.weixin.qq.com/s/rsDC-6paC5zN4sepWd5LqQ

近期在项目考虑在内存中保存从数据库加载的配置数据的方案,初步考虑采用map来保存。Go语言中有两个map,一个是Go语言原生的map类型,而另外一种则是在Go 1.9版本新增到标准库中的sync.Map

一. 原生map的“先天不足”

对于已经初始化了的原生map,我们可以尽情地对其进行并发读:

// github.com/bigwhite/experiments/inside-syncmap/concurrent_builtin_map_read.go

package main

import (
    "fmt"
    "math/rand"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    var m = make(map[int]int, 100)

    for i := 0; i < 100; i++ {
        m[i] = i
    }

    wg.Add(10)
    for i := 0; i < 10; i++ {
        // 并发读
        go func(i int) {
            for j := 0; j < 100; j++ {
                n := rand.Intn(100)
                fmt.Printf("goroutine[%d] read m[%d]: %d\n", i, n, m[n])
            }
            wg.Done()
        }(i)
    }
    wg.Wait()
}

但原生map一个最大的问题就是不支持多goroutine并发写。Go runtime内置对原生map并发写的检测,一旦检测到就会以panic的形式阻止程序继续运行,比如下面这个例子:

// github.com/bigwhite/experiments/inside-syncmap/concurrent_builtin_map_write.go

package main

import (
        "math/rand"
        "sync"
)

func main() {
        var wg sync.WaitGroup
        var m = make(map[int]int, 100)

        for i := 0; i < 100; i++ {
                m[i] = i
        }

        wg.Add(10)
        for i := 0; i < 10; i++ {
                // 并发写
                go func(i int) {
                        for n := 0; n < 100; n++ {
                                n := rand.Intn(100)
                                m[n] = n
                        }
                        wg.Done()
                }(i)
        }
        wg.Wait()
}

运行上面这个并发写的例子,我们很大可能会得到下面panic:

$go run concurrent_builtin_map_write.go
fatal error: concurrent map writes

... ...

原生map的“先天不足”让其无法直接胜任某些场合的要求,于是gopher们便寻求其他路径。一种路径无非是基于原生map包装出一个支持并发读写的自定义map类型,比如,最简单的方式就是用一把互斥锁(sync.Mutex)同步各个goroutine对map内数据的访问;如果读多写少,还可以利用读写锁(sync.RWMutex)来保护map内数据,减少锁竞争,提高并发读的性能。很多第三方map的实现原理也大体如此。

另外一种路径就是使用sync.Map

二. sync.Map的原理简述

按照官方文档,sync.Map是goroutine-safe的,即多个goroutine同时对其读写都是ok的。和第一种路径的最大区别在于,sync.Map对特定场景做了性能优化,一种是读多写少的场景,另外一种多个goroutine读/写/修改的key集合没有交集。

下面是两种技术路径的性能基准测试结果对比(macOS(4核8线程) go 1.14):

// 对应的源码在https://github.com/bigwhite/experiments/tree/master/go19-examples/benchmark-for-map下面

$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/go19-examples/benchmark-for-map
BenchmarkBuiltinMapStoreParalell-8           7945152           179 ns/op
BenchmarkSyncMapStoreParalell-8              3523468           387 ns/op
BenchmarkBuiltinRwMapStoreParalell-8         7622342           190 ns/op
BenchmarkBuiltinMapLookupParalell-8          7319148           163 ns/op
BenchmarkBuiltinRwMapLookupParalell-8       21800383            55.2 ns/op
BenchmarkSyncMapLookupParalell-8            70512406            18.5 ns/op
BenchmarkBuiltinMapDeleteParalell-8          8773206           174 ns/op
BenchmarkBuiltinRwMapDeleteParalell-8        5424912           214 ns/op
BenchmarkSyncMapDeleteParalell-8            49899008            23.7 ns/op
PASS
ok      github.com/bigwhite/experiments/go19-examples/benchmark-for-map    15.727s

我们看到:sync.Map在读和删除两项性能基准测试上的数据都大幅领先使用sync.Mutex或RWMutex包装的原生map,仅在写入一项上存在一倍的差距。sync.Map是如何实现如此高的读取性能的呢?简单说:空间换时间+读写分离+原子操作(快路径)。

sync.Map底层使用了两个原生map,一个叫read,仅用于读;一个叫dirty,用于在特定情况下存储最新写入的key-value数据:

img{512x368}

图:sync.Map内置两个原生map

read(这个map)好比整个sync.Map的一个“高速缓存”,当goroutine从sync.Map中读取数据时,sync.Map会首先查看read这个缓存层是否有用户需要的数据(key是否命中),如果有(命中),则通过原子操作将数据读取并返回,这是sync.Map推荐的快路径(fast path),也是为何上面基准测试结果中读操作性能极高的原因。

三. 通过实例深入理解sync.Map的原理

sync.Map源码(Go 1.14版本)不到400行,应该算是比较简单的了。但对于那些有着“阅读源码恐惧症”的gopher来说,我们可以通过另外一种研究方法:实例法,并结合些许源码来从“黑盒”角度理解sync.Map的工作原理。这种方法十分适合那些相对独立、可以从标准库中“单独”取出来的包,而sync.Map就是这样的包。

首先,我们将sync.Map从标准库源码目录中拷贝一份,放入本地~/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap/sync下面,得益于go module的引入,我们在~/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap目录下面建立go.mod文件:

module github.com/bigwhite/go

go 1.14

这样我们就可以通过github.com/bigwhite/go/sync包路径导入module:github.com/bigwhite/go下面的sync包了。

接下来,我们给位于~/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap/sync下面的map.go中(sync.Map包的副本)添加一个Map类型的新方法Dump

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/sync/map.go

func (m *Map) Dump() {
        fmt.Printf("=====> sync.Map:\n")
        // dump read
        read, ok := m.read.Load().(readOnly)
        fmt.Printf("\t read(amended=%v):\n", read.amended)
        if ok {
                // dump readOnly's map
                for k, v := range read.m {
                        fmt.Printf("\t\t %#v:%#v\n", k, v)
                }
        }

        // dump dirty
        fmt.Printf("\t dirty:\n")
        for k, v := range m.dirty {
                fmt.Printf("\t\t %#v:%#v\n", k, v)
        }

        // dump miss
        fmt.Printf("\t misses:%d\n", m.misses)

        // dump expunged
        fmt.Printf("\t expunged:%#v\n", expunged)
        fmt.Printf("<===== sync.Map\n")
}

这个方法将打印Map的内部状态以及read、dirty两个原生map中的所有key-value对,这样我们在初始状态、store key-value后、load key以及delete key后通过Dump方法输出sync.Map状态便可以看到不同操作后sync.Map内部的状态变化,从而间接了解sync.Map的工作原理。下面我们就分情况剖析sync.Map的行为特征。

1. 初始状态

sync.Map是零值可用的,我们可以像下面这样定义一个sync.Map类型变量,并无需做显式初始化(关于零值可用,在我的Go专栏《改善Go语言编程质量的50个有效实践》中有专门的一节详述,有兴趣的gopher可以订阅学习^_^)。

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go

var m sync.Map

我们通过Dump输出初始状态下的sync.Map的内部状态:

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go

func main() {
        var m sync.Map
        fmt.Println("sync.Map init status:")
        m.Dump()

        ... ...

}

运行后,输出如下:

sync.Map init status:
=====> sync.Map:
     read(amended=false):
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc0001101e0)
<===== sync.Map

在初始状态下,dirty和read两个内置map内都无数据。expunged是一个哨兵变量(也是一个包内的非导出变量),它在sync.Map包初始化时就有了一个固定的值。该变量在后续用于元素删除场景(删除的key并不立即从map中删除,而是将其value置为expunged)以及load场景。如果哪个key值对应的value值与explunged一致,说明该key已经被map删除了(即便该key所占用的内存资源尚未释放)。

// map.go

var expunged = unsafe.Pointer(new(interface{}))

2. 写入数据(store)

下面,我们向Map写入一条数据:

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go

type val struct {
        s string
}

func main() {
        ... ...
        val1 := &val{"val1"}
        m.Store("key1", val1)
        fmt.Println("\nafter store key1:")
        m.Dump()

        ... ...

}

我们看一下存入新数据后,Map内部的状态:

after store key1:
=====> sync.Map:
     read(amended=true):
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000108080)}
     misses:0
     expunged:(unsafe.Pointer)(0xc000108040)
<===== sync.Map

我们看到写入(key1,value1)后,Map中有两处变化,一处是dirty map,新写入的数据存储在dirty map中;第二处是read中的amended值由false变为了true,表示dirty map中存在某些read map还没有的key

3. dirty提升(promoted)为read

此时,如果我们调用一次sync.Map的Load方法,无论传给Load的key值是否为”key1″还是其他,sync.Map内部都会发生较大变化,我们来看一下:

// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go

        m.Load("key2") //这里我们尝试load key="key2"
        fmt.Println("\nafter load key2:")
        m.Dump()

下面是Load方法调用后Dump方法输出的内容:

after load key2:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

我们看到:原dirty map中的数据被提升(promoted)到read map中了,提升后amended值重新变回false

结合sync.Map中Load方法的源码,我们得出如下sync.Map的工作原理:当Load方法在read map中没有命中(miss)传入的key时,该方法会再次尝试在dirty中继续匹配key;无论是否匹配到,Load方法都会在锁保护下调用missLocked方法增加misses的计数(+1);如果增加完计数的misses值大于等于dirty map中的元素个数,则会将dirty中的元素整体提升到read:

// $GOROOT/src/sync/map.go

func (m *Map) missLocked() {
        m.misses++  //计数+1
        if m.misses < len(m.dirty) {
                return
        }
        m.read.Store(readOnly{m: m.dirty})  // dirty提升到read
        m.dirty = nil  // dirty置为nil
        m.misses = 0 // misses计数器清零
}

为了验证上述promoted的条件,我们再来做一组实验:

        val2 := &val{"val2"}
        m.Store("key2", val2)
        fmt.Println("\nafter store key2:")
        m.Dump()

        val3 := &val{"val3"}
        m.Store("key3", val3)
        fmt.Println("\nafter store key3:")
        m.Dump()

        m.Load("key1")
        fmt.Println("\nafter load key1:")
        m.Dump()

        m.Load("key2")
        fmt.Println("\nafter load key2:")
        m.Dump()

        m.Load("key2")
        fmt.Println("\nafter load key2 2nd:")
        m.Dump()

        m.Load("key2")
        fmt.Println("\nafter load key2 3rd:")
        m.Dump()

在完成一次promoted动作之后,我们又向sync.Map中写入两个key:key2和key3,并在后续Load一次key1并连续三次Load key2,下面是Dump方法的输出结果:

after store key2:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after store key3:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key1:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key2:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     misses:1
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key2 2nd:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     misses:2
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key2 3rd:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010290)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

我们看到在写入key2这条数据后,dirty中不仅存储了key2这条数据,原read中的key1数据也被复制了一份存入到dirty中。这个操作是由sync.Map的dirtyLocked方法完成的:

// $GOROOT/src/sync/map.go

func (m *Map) dirtyLocked() {
        if m.dirty != nil {
                return
        }

        read, _ := m.read.Load().(readOnly)
        m.dirty = make(map[interface{}]*entry, len(read.m))
        for k, e := range read.m {
                if !e.tryExpungeLocked() {
                        m.dirty[k] = e
                }
        }
}

前面我们提到过,promoted(dirty -> read)是一个整体的指针交换操作,promoted时,sync.Map直接将原dirty指针store给read并将自身置为nil,因此sync.Map要保证amended=true时,dirty中拥有整个Map的全量数据,这样在下一次promoted(dirty -> read)时才不会丢失数据。不过dirtyLocked是通过一个迭代实现的元素从read到dirty的复制,如果Map中元素规模很大,这个过程付出的损耗将很大,并且这个过程是在锁保护下的。

在存入key3后,我们调用Load方法先load了key1,由于key1在read中有记录,因此此次load命中了,走的是快路径,对Map状态没有任何影响。

之后,我们又Load了key2,key2不在read中,因此产生了一次miss。misses增加计数后的值为1,而此时dirty中的元素数量为3,不满足promote的条件,于是没有执行promote操作。后续我们又连续进行了两次key2的Load操作,产生了两次miss事件后,misses的计数值等于了dirty中的元素数量,于是promote操作被执行,dirty map整体被置换给read,自己则变成了nil。

4. 更新已存在的key

我们再来看一下更新已存在的key的值的情况。首先是该key仅存在于read中(刚刚promote完毕),而不在dirty中。我们更新这时仅在read中存在的key2的值:

        val2_1 := &val{"val2_1"}
        m.Store("key2", val2_1)
        fmt.Println("\nafter update key2(in read, not in dirty):")
        m.Dump()

下面是Dump输出的结果:

after update key2(in read, not in dirty):
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc00008e1e0)
<===== sync.Map

我们看到sync.Map直接更新了位于read中的key2的值(entry.storeLocked方法实现的),dirty和其他字段没有受到影响。

第二种情况是该key刚store到dirty中,尚未promote,不在read中。我们新增一个key4,并更新其值:

        val4 := &val{"val4"}
        m.Store("key4", val4)
        fmt.Println("\nafter store key4:")
        m.Dump()

        val4_1 := &val{"val4_1"}
        m.Store("key4", val4_1)
        fmt.Println("\nafter update key4(not in read, in dirty):")
        m.Dump()

dump方法的输出结果如下:

after store key4:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
         "key4":&smap.entry{p:(unsafe.Pointer)(0xc00008e310)}
     misses:0
     expunged:(unsafe.Pointer)(0xc00008e1e0)
<===== sync.Map

after update key4(not in read, in dirty):
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
         "key4":&smap.entry{p:(unsafe.Pointer)(0xc00008e330)}
     misses:0
     expunged:(unsafe.Pointer)(0xc00008e1e0)
<===== sync.Map

我们看到,sync.Map同样是直接将key4对应的value重新设置为新值(val4_1)。

5. 删除key

为了方便查看,我们将上述Map状态回滚到刚刚promote(dirty -> read)完的时刻,即:

after load key2 3rd:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc00008e220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc00008e270)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc00008e1e0)
<===== sync.Map

删除key也有几种情况,我们分别来看一下:

  • 删除的key仅存在于read中

我们删除上面Map中仅存在于read中的key2:

        m.Delete("key2")
        fmt.Println("\nafter delete key2:")
        m.Dump()

删除后的Dump结果如下:

after delete key2:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000010240)}
         "key2":&smap.entry{p:(unsafe.Pointer)(nil)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

我们看到sync.Map并没有删除key2,而是将其value置为nil。

  • 删除的key仅存在于dirty中

为了构造初仅存在于dirty中的key,我们向sync.Map写入新数据key4,然后再立刻删除它

        val4 := &val{"val4"}
        m.Store("key4", val4)
        fmt.Println("\nafter store key4:")
        m.Dump()

        m.Delete("key4")
        fmt.Println("\nafter delete key4:")
        m.Dump()

上述代码的Dump结果如下:

after store key4:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000104220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc0001041e0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
     dirty:
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000104220)}
         "key4":&smap.entry{p:(unsafe.Pointer)(0xc0001042f0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
     misses:0
     expunged:(unsafe.Pointer)(0xc0001041e0)
<===== sync.Map

after delete key4:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000104220)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc0001041e0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
     dirty:
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(0xc000104220)}
     misses:0
     expunged:(unsafe.Pointer)(0xc0001041e0)
<===== sync.Map

我们看到:和仅在read中的情况不同(仅将value设置为nil),仅存在于dirty中的key被删除后,该key就不再存在了。这里还有一点值得注意的是:当向dirty写入key4时,dirty会复制read中的未被删除的元素,由于key2已经被删除,因此顺带将read中的key2对应的value设置为哨兵(expunged),并且该key不会被加入到dirty中。直到下一次promote,该key才会被回收(因为read被交换指向新的dirty,原read指向的内存将被GC)。

  • 删除的key既存在于read,也存在于dirty中

目前上述sync.Map实例中既存在于read,也存在于dirty中的key有key1和key3(key2已经被删除),我们这里以删除key1为例:

after delete key1:
=====> sync.Map:
     read(amended=true):
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc0001041e0)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
     dirty:
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0001042a0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
     misses:0
     expunged:(unsafe.Pointer)(0xc0001041e0)
<===== sync.Map

我们看到删除key1后,read和dirty两个map中的key1均没有真正删除,而是将其value设置为nil。

我们再触发一次promote:连续调用两次导致read miss的LOAD:

        m.Load("key5")
        fmt.Println("\nafter load key5:")
        m.Dump()

        m.Load("key5")
        fmt.Println("\nafter load key5 2nd:")
        m.Dump()

调用后的Dump输出如下:

after load key5:
=====> sync.Map:
     read(amended=true):
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
         "key2":&smap.entry{p:(unsafe.Pointer)(0xc000010200)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     dirty:
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
     misses:1
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

after load key5 2nd:
=====> sync.Map:
     read(amended=false):
         "key1":&smap.entry{p:(unsafe.Pointer)(nil)}
         "key3":&smap.entry{p:(unsafe.Pointer)(0xc0000102c0)}
     dirty:
     misses:0
     expunged:(unsafe.Pointer)(0xc000010200)
<===== sync.Map

我们看到虽然dirty中的key1已经处于被删除状态,但它仍算作dirty元素的个数,因此第二次miss才会触发promote。promote后,dirty被赋值给read,因此原dirty中的key1元素就顺带进入到read中,只能等下次写入一个不存在的新key时才能被置为哨兵值,并在下一次promote时才能被真正删除释放。

四. 小结

通过实例法,我们大致得到了sync.Map的工作原理和行为特征,从这些结果来看sync.Map并非是一个可应用于所有场合的goroutine-safe的map实现,但在读多写少的情况下,sync.Map才能发挥出最大的效能。

本文涉及代码可以在这里 https://github.com/bigwhite/experiments/tree/master/inside-syncmap 下载。


我的Go技术专栏:“改善Go语⾔编程质量的50个有效实践”上线了,欢迎大家订阅学习!

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats