Mutex(MUTualEx)-互斥锁是一种可以保证每次只有一个goroutine访问贡献资源的方法。这个资源可以是一段程序代码、一个整数、一个map、一个struct、一个channel或其他任何东西。通过观察Mutex的源代码实现,可以将Mutex看作是一个队列(FIFO/LIFO),具体看后面的详细描述

当前go版本:1.24

快速上手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"fmt"
"sync"
)

type Container struct {
mu sync.Mutex
counters map[string]int
}

func (c *Container) inc(name string) {
c.mu.Lock() // 互斥锁,获取失败等待挂起
defer c.mu.Unlock() // 解锁
c.counters[name]++ // 共享资源
}

func main() {
c := Container{
counters: map[string]int{"a": 0, "b": 0},
}

var wg sync.WaitGroup

doIncrement := func(name string, n int) {
for i := 0; i < n; i++ {
c.inc(name)
}
wg.Done()
}

wg.Add(3)
go doIncrement("a", 10000)
go doIncrement("a", 10000)
go doIncrement("b", 10000)

wg.Wait()
fmt.Println(c.counters)
}

上述代码运行结果如下

1
2
3
4
go run ./main.go

# 输出如下
# map[a:20000 b:10000]
阅读全文 »

sync.Cond经常用在多个 goroutine 等待,一个 goroutine 通知(事件发生)的场景。如果是一个通知,一个等待,使用互斥锁或 channel 就能搞定了。底层实现基于信号量semaphore

当前go版本:1.24

快速上手

以下展示一个sync.Cond的使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"sync"
)

var shared = make(map[string]interface{})

func main() {
var wg sync.WaitGroup
wg.Add(2)

cond := sync.NewCond(&sync.Mutex{})

// reader 打印shared[key]
reader := func(key string) {
cond.L.Lock()
// 等待,直到shared有数据
for len(shared) == 0 {
cond.Wait() // Wait内部会暂时解锁/加锁
}
fmt.Println(shared[key])
cond.L.Unlock()
wg.Done()
}

// 创建两个goroutine
go reader("rsc1")
go reader("rsc2")

// writer
cond.L.Lock()
// 写入
shared["rsc1"] = "foo"
shared["rsc2"] = "bar"
// 通知所有goroutine
cond.Broadcast()
cond.L.Unlock()

wg.Wait()
}
阅读全文 »

并发情况下,如果需要等待所有的goroutine完成任务,需要使用Waitgroup等待

当前go版本:1.24

快速上手

先简单列举一个使用案例,了解Waitgroup的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"sync"
"time"
)

func worker(id int) {
fmt.Printf("Worker %d starting\n", id)

time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}

func main() {
var wg sync.WaitGroup

for i := 1; i <= 5; i++ {
// counter++
wg.Add(1)

go func() {
// counter--
defer wg.Done()
worker(i)
}()
}

// wait
wg.Wait()
}

上述代码执行后,系统输出如下,可以看到系统等待5个goroutine完成任务后才退出

1
2
3
4
5
6
7
8
9
10
11
12
13
go run ./main.go

# 输出如下
# Worker 5 starting
# Worker 2 starting
# Worker 1 starting
# Worker 3 starting
# Worker 4 starting
# Worker 4 done
# Worker 5 done
# Worker 2 done
# Worker 3 done
# Worker 1 done
阅读全文 »

sync/atomic标准库包中提供的原子操作。原子操作是无锁的,直接通过CPU指令实现。

当你想要在多个goroutine中无锁访问一个变量时,就可以考虑使用atomic包提供的数据类型实现

当前go版本:1.24

快速上手

以下是一个使用atomic.Uint64数据类型实现的计数器,它确保了多个goroutine按顺序正确更新数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"sync"
"sync/atomic"
)

func main() {

var ops atomic.Uint64

var wg sync.WaitGroup

for i := 0; i < 50; i++ {
wg.Add(1)

go func() {
for c := 0; c < 1000; c++ {

ops.Add(1)
}

wg.Done()
}()
}

wg.Wait()

fmt.Println("ops:", ops.Load())
}
阅读全文 »

sync.Pool-临时对象池,是golang一个很关键的数据结构,通过复用历史对象,缓解因频繁创建、删除对象而导致的内存分配压力、GC压力,在社区中被广泛使用,有如go-gin、kubernetes等

当前go版本:1.24

快速上手

下面展示一个简单的使用示例,用于帮助用户快速上手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
"sync"
)

type JobState int

const (
JobStateFresh JobState = iota
JobStateRunning
JobStateRecycled
)

type Job struct {
state JobState
}

func (j *Job) Run() {
switch j.state {
case JobStateRecycled:
fmt.Println("this job came from the pool")
case JobStateFresh:
fmt.Println("this job just got allocated")
}

j.state = JobStateRunning
}

func main() {
// 创建一个对象池
pool := &sync.Pool{
New: func() any {
return &Job{state: JobStateFresh}
},
}

// 获取一个对象,可以是新建的或者是历史使用过的
job := pool.Get().(*Job)

// 执行业务代码
job.Run()

// reset状态并放回池子里,方便下次使用
job.state = JobStateRecycled
pool.Put(job)
}
阅读全文 »

如果要实现如Singleton、Lazy Initialization模式,那么你需要了解sync.Once,它可以用于保证如:只加载一次配置文件、只初始化一次数据库连接等,此外它还可以帮助实现更好的Plugin封装

当前go版本:1.24

快速上手

以下代码展示了如何使用sync.Once实现singleton模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"sync"
"time"
)

type Singleton struct {
data string
}

var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
once.Do(func() {
fmt.Println("Creating Singleton instance")
instance = &Singleton{data: "I'm the only one!"}
})
return instance
}

func main() {
for i := 0; i < 5; i++ {
go func() {
fmt.Printf("%p\n", GetInstance())
}()
}

// Wait for goroutines to finish
time.Sleep(3 * time.Second)
}
阅读全文 »

map/哈希表,是golang常用的数据结构之一,也充当set数据结构的存在,相对slice要复杂很多。从1.24开始,swiss table替代noswiss成为默认实现,swiss与noswiss区别在于,swiss使用开放地址法,noswiss使用拉链法

当前go版本:1.24

swiss map的开关放在文件src/internal/buildcfg/exp.go的函数ParseGOEXPERIMENT

数据结构

todo:文章图片待补充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// table-groups是二维数组,算上slot的话是三维
//
// map_header -> table1 -> group1(8个slot)
// -> group2
// ...
// -> group127
//
// -> table2
// -> ...
// -> tableN
//
// 当hint<=8时,map_header直接指向一个group,全量扫描操作
// 当hint>8 and hint<=1024*7/8时,一个table,2~128个group
// 当hint>1024*7/8时,多个table,多个group
//
// hash高B位 - 用于定位table
// h1-hash高57位 - 用于定位group
// h2-hash低7位 - 用于匹配hash,类似tophash

核心数据结构包括Map、table、groupsReference、groupReference,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// src/internal/runtime/maps/map.go
type Map struct {
used uint64 // 已用slot,当数据量<=8时用来替换table的used使用
seed uintptr // 哈希函数种子
dirPtr unsafe.Pointer // table数组指针/group指针
dirLen int // table数组大小
globalDepth uint8 // log2(dirLen)(相当于旧版的B)
globalShift uint8 // 64-globalDepth,高B位用做table的索引
writing uint8 // 是否正在写入,乐观锁
clearSeq uint64 // 执行过多少次clear,扩容时,获取数据判断用
}

// src/internal/runtime/maps/table.go
type table struct {
used uint16 // 已用slot,最多能写入1024*7/8=896个slot
capacity uint16 // slot容量 <=1024(由hint算出,2的乘方向上取整)
growthLeft uint16 // 可用slot,与used相反,初始值最大为896
localDepth uint8 // >globalDepth?分裂/遍历时使用
index int // 上层directory数组中的index(-1-过期,作用类似localDepth)
groups groupsReference // group数组,8个slot为一组,最多1024/8=128组
}

// src/internal/runtime/maps/group.go
type groupsReference struct {
data unsafe.Pointer // group数组,8个slot为一组,具体结构看下方
lengthMask uint64 // 长度固定为2^N,因此mask=2^N-1
}

type groupReference struct {
// 结构如下
//
// type group struct {
// ctrls ctrlGroup // 8个8bit的ctrl,共三种状态
// slots [abi.SwissMapGroupSlots]slot // 8个slot(key/value对)
// }
//
// 三种状态如下:
// empty: 1 0 0 0 0 0 0 0
// deleted: 1 1 1 1 1 1 1 0
// full: 0 h h h h h h h // h represents the H2 hash bits
//
// type slot struct {
// key typ.Key // 键
// elem typ.Elem // 值
// }
data unsafe.Pointer // ctrls数组+slots数组
// 内存布局如下(C语言开发者真的很喜欢这种内存布局啊)
// | ctrls | slots |
// |ctrl7|...|ctrl0|slot0|...|slot7|
}
阅读全文 »

map/哈希表,是golang常用的数据结构之一,也充当set数据结构的存在,相对slice要复杂很多。从1.24开始,swiss table替代noswiss成为默认实现,swiss与noswiss区别在于,swiss使用开放地址法,noswiss使用拉链法

当前go版本:1.23

数据结构

todo:文章图片待补充

1
2
3
4
5
6
7
8
9
10
11
12
13
// 
// hmap -> oldbuckets
// -> buckets -> bmap0(8个key/value对)
// -> bmap1 -> overflow0(bmapX) -> overflow1(bmapZ)
// -> ...
// -> bmapM -> overflow0(bmapY)
// -> bmapN
//
// -> extra.overflow -> bmapX(pre-alloc)
// -> bmapY(pre-alloc)
// -> ...
// -> bmapZ(new-alloc)
//

map的数据结构如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/runtime/map.go
type hmap struct {
count int // 元素数量-key/value对
flags uint8 // 1-iter正在使用buckets字段 2-iter正在使用oldbuckets字段 4-正在写入 8-同等大小扩容
B uint8 // buckets数量=(loadFactor * 2^B)
noverflow uint16 // 统计溢出buckets的数量,当B大于15时不是精确值
hash0 uint32 // 哈希种子,计算hash用
buckets unsafe.Pointer // buckets数组
oldbuckets unsafe.Pointer // 旧buckets数组,扩容时用
nevacuate uintptr // 下一个未疏散的bucket索引
clearSeq uint64 // 执行过多少次clear
extra *mapextra // 可选字段,不是每个map都需要,同时也需要为gc考虑
}

// key/value都不是指针才会使用下面几个字段
type mapextra struct {
overflow *[]*bmap // 溢出buckets,buckets链接使用
oldoverflow *[]*bmap // 溢出buckets,oldbuckets链接使用,扩容时
nextOverflow *bmap // 下一个可用/未使用overflow的索引
}

// bucket结构,可存储8个key/value对及其他数据,编译时自动补充其余结构,真实结构如下
// A "bucket" is a "struct" {
// tophash [abi.MapBucketCount]uint8 // hash的高8位
// keys [abi.MapBucketCount]keyType // 所有key
// elems [abi.MapBucketCount]elemType // 所有value
// overflow *bucket // 下一个溢出桶指针
// }
// 详细可见MapBucketType函数(src/cmd/compile/internal/reflectdata/reflect.go)
type bmap struct {
// abi.MapBucketCount=8
// 0-默认状态 1-已删除 2-疏散到x 3-疏散到y 4-不需要疏散 5-guard xyz(>5)-正常tophash值
// 状态转移如下:
// 0/xyz -> 2/3/4 => 如果所有bucket都疏散完毕,会一次性清空释放
// 0/xyz -> 1 删除 => 如果idx+1的状态是0,则向前寻找状态为1数据并改为0
tophash [abi.MapBucketCount]uint8
}

上面数据结构中中比较关键的是

  • hmap - header部份,var变量存储的也是这部份
  • buckets - 指向一片连续内存区域,bucket数组
  • bmap - bucket的具体实现,可以存储8个key/value对,尾部overflow是溢出bucket的指针
阅读全文 »

slice/切片-动态数组,golang常用的数据结构之一,相对于数组,slice可以追加元素,在容量不足时自动扩容

当前go版本:1.24

数据结构

todo:文章图片待补充

slice数据结构如下所示

1
2
3
4
5
6
// src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}

其中

  • array - 指向一片连续内存区域的第一个元素
  • len - 已有元素数量
  • cap - 可容纳元素总数量

初始化

slice初始化方式有三种

  1. 使用字面量创建新切片
  2. 使用关键字make创建切片
  3. 通过下标获取数组或切片的一部份
1
2
3
4
5
6
// len=3 cap=3
var v1 = []int{1, 2, 3}
// len=10 cap=10
var v2 = make([]int, 10) // var v2 = make([]int, 0, 10)
// len=4 cap=9
var v3 = v2[1:5]
阅读全文 »

Ubuntu 24.04为例,安装nerdctl以及containerd

前置准备

ubuntu

  • 安装uidmap
1
sudo apt update && sudo apt install -y uidmap
  • AppArmor配置

由于懒人安装方式跟单独安装方式bin目录不一致,配置内容会有些许差异,放在下面章节各自介绍

依赖包

阅读全文 »
0%