github.com/bruceshao/lockfree@v1.1.3-0.20230816090528-e89824c0a6e9/README.md (about) 1 ## Lockfree 2 3 > 如果想使用低于go1.18版本则可以引入tag:1.0.9或branch:below-version1.18 4 5 > 通过条件编译支持386平台,但性能测试发现比较差,因此不建议使用 6 7 ### 1. 简介 8 #### 1.1. 为什么要写Lockfree 9 在go语言中一般都是使用chan作为消息传递的队列,但在实际高并发的环境下使用发现chan存在严重的性能问题,其直接表现就是将对象放入到chan中时会特别耗时, 10 即使chan的容量尚未打满,在严重时甚至会产生几百ms还无法放入到chan中的情况。 11 12 ![放大.jpg](images%2F%E6%94%BE%E5%A4%A7.jpg) 13 14 #### 1.2. chan基本原理 15 16 ##### 1.2.1. chan基本结构 17 18 chan关键字在编译阶段会被编译为runtime.hchan结构,它的结构如下所示: 19 20 ![chan结构.jpg](images%2Fchan%E7%BB%93%E6%9E%84.jpg) 21 22 其中sudog是一个比较常见的结构,是对goroutine的一个封装,它的主要结构: 23 24 ![sudog.jpg](images%2Fsudog.jpg) 25 26 27 ##### 1.2.2. chan写入流程 28 29 ![write.jpg](images%2Fwrite.jpg) 30 31 ##### 1.2.3. chan读取流程 32 33 ![read.jpg](images%2Fread.jpg) 34 35 #### 1.3. 锁 36 37 ##### 1.3.1. runtime.mutex 38 chan结构中包含了一个lock字段:`lock mutex`。 39 这个lock看名字就知道是一个锁,当然它不是我们业务中经常使用的sync.Mutex,而是一个`runtime.mutex`。 40 这个锁是一个互斥锁,在linux系统中它的实现是futex,在没有竞争的情况下,会退化成为一个自旋操作,速度非常快,但是当竞争比较大时,它就会在内核中休眠。 41 42 futex的基本原理如下图: 43 ![futex.jpg](images%2Ffutex.jpg) 44 45 当竞争非常大时,对于chan而言其整体大部分时间是出于系统调用上,所以性能下降非常明显。 46 47 ##### 1.3.2. sync.Mutex 48 49 而`sync.Mutex`包中的设计原理如下图: 50 51 ![锁.jpg](images%2F%E9%94%81.jpg) 52 53 54 ### 2. Lockfree基本原理 55 56 #### 2.1. 模块及流程 57 58 > 在最新的设计中已经删除了available模块,转而使用ringBuffer中的对象(e)中的c(游标)标识写入状态。 59 60 ![lockfree.jpg](images%2Flockfree.jpg) 61 62 63 64 #### 2.2. 优化点 65 66 ##### 1) 无锁实现 67 68 内部所有操作都是通过原子变量(atomic)来操作,唯一有可能使用锁的地方,是提供给用户在RingBuffer为空时的等待策略,用户可选择使用chan阻塞 69 70 ##### 2) 单一消费协程 71 72 屏蔽掉消费端读操作竞争带来的性能损耗 73 74 ##### 3) 写不等待原则 75 76 符合写入快的初衷,当无法写入时会持续通过自旋和任务调度的方式处理,一方面尽量加快写入效率,另一方面则是防止占用太多CPU资源 77 78 ##### 4) 泛型加速 79 80 引入泛型,泛型与interface有很明显的区别,泛型是在编译阶段确定类型,这样可有效降低在运行时进行类型转换的耗时 81 82 ##### 5) 一次性内存分配 83 84 环状结构Ringbuffer实现对象的传递,通过确定大小的切片实现,只需要分配一次内存,不会涉及扩容等操作,可有效提高处理的性能 85 86 ##### 6) 运算加速 87 88 RingBuffer的容量为2的n次方,通过与运算来代替取余运算,提高性能 89 90 ##### 7) 并行位图 91 92 用原子位运算实现位图并行操作,在尽量不影响性能的条件下,降低内存消耗 93 94 并行位图的思路实现历程: 95 96 ![bitmap.jpg](images%2Fbitmap.jpg) 97 98 ##### 8) 缓存行填充 99 100 根据CPU高速缓存的特点,通过填充缓存行方式屏蔽掉伪共享问题。 101 102 缓存行填充应该是一个比较常见的问题,它的本质是因为CPU比较快,而内存比较慢,所以增加了高速缓存来处理: 103 104 ![padding1.jpg](images%2Fpadding1.jpg) 105 106 在两个Core共享同一个L3的情况下,如果同时进行修改,就会出现竞争关系(会涉及到缓存一致性协议:MESI): 107 108 ![padding2.jpg](images%2Fpadding2.jpg) 109 110 在Lockfree中有两个地方用到了填充: 111 112 ![padding3.jpg](images%2Fpadding3.jpg) 113 114 最新版本中只保留了cursor中的填充,在e中使用了游标。 115 116 ##### 9) Pointer替代切片 117 118 屏蔽掉切片操作必须要进行越界判断的逻辑,生成更高效机器码。 119 120 ![pointer.jpg](images%2Fpointer.jpg) 121 122 123 #### 2.3. 核心模块 124 125 ##### ringBuffer 126 具体对象的存放区域,通过数组(定长切片)实现环状数据结构,其中的数据对象是具体的结构体而非指针,这样可以一次性进行内存申请。 127 128 ##### stateDescriptor 129 130 > 注:最新的版本已将该对象删除,通过ringBuffer中e中的游标来描述状态。这样更充分利用了内存,降低了消耗。 131 132 状态描述符,定义了对应位置的数据状态,是可读还是可写。提供了三种方式: 133 134 + 1) 基于Uint32的切片:每个Uint32值描述一个位置,性能最高,但内存消耗最大; 135 136 + 2) 基于Bitmap:每个bit描述一个位置,性能最低,但内存消耗最小; 137 138 + 3) 基于Uint8的切片:每个Uint8值描述一个位置,性能适中,消耗也适中,最推荐的方式。 139 140 ##### sequencer 141 序号产生器,维护读和写两个状态,写状态具体由内部游标(cursor)维护,读取状态由自身维护,一个uint64变量维护。它的核心方法是next(),用于获取下个可以写入的游标。 142 143 ##### Producer 144 生产者,核心方法是Write,通过调用Write方法可以将对象写入到队列中。支持多个g并发操作,保证加入时处理的效率。 145 146 ##### consumer 147 消费者,这个消费者只会有一个g操作,这样处理的好处是可以不涉及并发操作,其内部不会涉及到任何锁,对于实际的并发操作由该g进行分配。 148 149 ##### blockStrategy 150 阻塞策略,该策略用于buf中长时间没有数据时,消费者阻塞设计。它有两个方法:block()和release()。前者用于消费者阻塞,后者用于释放。 151 系统提供了多种方式,不同的方式CPU资源占用和性能会有差别: 152 153 + 1) SchedBlockStrategy:调用runtime.Gosched()进行调度block,不需要release,为推荐方式; 154 155 + 2) SleepBlockStrategy:调用time.Sleep(x)进行block,可自定义休眠时间,不需要release,为推荐方式; 156 157 + 3) ProcYieldBlockStrategy:调用CPU空跑指令,可自定义空跑的指令数量,不需要release; 158 159 + 4) OSYieldBlockStrategy:操作系统会将对应M调度出去,等时间片重新分配后可执行,不需要release; 160 161 + 5) ChanBlockStrategy:chan阻塞策略,需要release,为推荐方式; 162 163 + 6) CanditionBlockStrategy:candition阻塞策略,需要release,为推荐方式; 164 165 其中1/2/5/6为推荐方式,如果性能要求比较高,则优先考虑2和1,否则建议试用5和6。 166 167 ##### EventHandler 168 事件处理器接口,整个项目中唯一需要用户实现的接口,该接口描述消费端收到消息时该如何处理,它使用泛型,通过编译阶段确定事件类型,提高性能。 169 170 ### 3. 使用方式 171 172 #### 3.1. 导入模块 173 可使用 `go get github.com/bruceshao/lockfree` 获取最新版本 174 175 #### 3.2. 代码调用 176 为了提升性能,Lockfree支持go版本1.18及以上,以便于支持泛型,Lockfree使用非常简单: 177 178 ```go 179 package main 180 181 import ( 182 "fmt" 183 "sync" 184 "sync/atomic" 185 "time" 186 187 "github.com/bruceshao/lockfree" 188 ) 189 190 var ( 191 goSize = 10000 192 sizePerGo = 10000 193 194 total = goSize * sizePerGo 195 ) 196 197 func main() { 198 // lockfree计时 199 now := time.Now() 200 201 // 创建事件处理器 202 handler := &eventHandler[uint64]{ 203 signal: make(chan struct{}, 0), 204 now: now, 205 } 206 207 // 创建消费端串行处理的Lockfree 208 lf := lockfree.NewLockfree[uint64]( 209 1024*1024, 210 handler, 211 lockfree.NewSleepBlockStrategy(time.Millisecond), 212 ) 213 214 // 启动Lockfree 215 if err := lf.Start(); err != nil { 216 panic(err) 217 } 218 219 // 获取生产者对象 220 producer := lf.Producer() 221 222 // 并发写入 223 var wg sync.WaitGroup 224 wg.Add(goSize) 225 for i := 0; i < goSize; i++ { 226 go func(start int) { 227 for j := 0; j < sizePerGo; j++ { 228 err := producer.Write(uint64(start*sizePerGo + j + 1)) 229 if err != nil { 230 panic(err) 231 } 232 } 233 wg.Done() 234 }(i) 235 } 236 237 // wait for producer 238 wg.Wait() 239 240 fmt.Printf("producer has been writed, write count: %v, time cost: %v \n", total, time.Since(now).String()) 241 242 // wait for consumer 243 handler.wait() 244 245 // 关闭Lockfree 246 lf.Close() 247 } 248 249 type eventHandler[T uint64] struct { 250 signal chan struct{} 251 gcounter uint64 252 now time.Time 253 } 254 255 func (h *eventHandler[T]) OnEvent(v uint64) { 256 cur := atomic.AddUint64(&h.gcounter, 1) 257 if cur == uint64(total) { 258 fmt.Printf("eventHandler has been consumed already, read count: %v, time cose: %v\n", total, time.Since(h.now)) 259 close(h.signal) 260 return 261 } 262 263 if cur%10000000 == 0 { 264 fmt.Printf("eventHandler consume %v\n", cur) 265 } 266 } 267 268 func (h *eventHandler[T]) wait() { 269 <-h.signal 270 } 271 ``` 272 273 ### 4. 性能对比 274 275 #### 4.1. 简述 276 277 在实际测试中发现,如果lockfree和chan同时跑的话会有一些影响,lockfree的表现基本是正常的,和chan同时跑的时候性能基本是下降的。 278 但chan比较奇怪,和lockfree一起跑的时候性能比chan自身跑性能还高。目前正在排查此问题,但不影响使用。 279 280 main包下提供了测试的程序,可自行进行性能测试(假设编译后的二进制为lockfree): 281 282 + 1)单独测试lockfree:./lockfree lockfree [time],加入time会有时间分布; 283 + 2)单独测试chan:./lockfree chan [time],加入time会有时间分布; 284 + 3)合并测试lockfree和chan:./lockfree [all] [time],使用time时,前面必须加all参数,只进行测试,不关注时间分布的话,可直接调用./lockfree; 285 286 为描述性能,除了时间外,定义了**QR**(Quick Ratio,快速率)的指标,该指标描述的是写入时间在1微秒以内的操作占所有操作的比值。 287 自然的,QR越大,性能越高。 288 289 #### 4.2. 软硬件测试环境 290 291 CPU信息如下(4 * 2.5GHz): 292 ```shell 293 [root@VM]# lscpu 294 Architecture: x86_64 295 CPU op-mode(s): 32-bit, 64-bit 296 Byte Order: Little Endian 297 CPU(s): 4 298 On-line CPU(s) list: 0-3 299 Thread(s) per core: 1 300 Core(s) per socket: 4 301 Socket(s): 1 302 NUMA node(s): 1 303 Vendor ID: GenuineIntel 304 CPU family: 6 305 Model: 94 306 Model name: Intel(R) Xeon(R) Gold 6133 CPU @ 2.50GHz 307 Stepping: 3 308 CPU MHz: 2494.120 309 BogoMIPS: 4988.24 310 Hypervisor vendor: KVM 311 Virtualization type: full 312 L1d cache: 32K 313 L1i cache: 32K 314 L2 cache: 4096K 315 L3 cache: 28160K 316 NUMA node0 CPU(s): 0-3 317 ``` 318 319 内存信息(8G): 320 ```shell 321 [root@VM]# free -m 322 total used free shared buff/cache available 323 Mem: 7779 405 6800 116 573 7216 324 Swap: 0 0 0 325 ``` 326 327 操作系统(centos 7.2): 328 ```shell 329 [root@VM]# cat /etc/centos-release 330 CentOS Linux release 7.2 (Final) 331 [root@VM]# uname -a 332 Linux VM-219-157-centos 3.10.107-1-tlinux2_kvm_guest-0056 #1 SMP Wed Dec 29 14:35:09 CST 2021 x86_64 x86_64 x86_64 GNU/Linux 333 ``` 334 335 云厂商:腾讯云。 336 337 338 #### 4.3. 写入性能对比 339 340 设定buffer大小为1024 * 1024,无论是lockfree还是chan都是如此设置。其写入的时间对比如下: 341 342 其中 100 * 10000表示有100个goroutine,每个goroutine写入10000次,其他的依次类推。 343 344 all(lockfree/chan)表示在lockfree和chan同时跑的情况下,其分别的时间占比情况。 345 346 | 类型 | 100 * 10000 | 500 * 10000 | 1000 * 10000 | 5000 * 10000 | 10000 * 10000 | 347 |---------------|-------------|-------------|--------------|--------------|---------------| 348 | lockfree | 67ms | 306ms | 676ms | 3779ms | 7703ms | 349 | chan | 116ms | 1991ms | 4709ms | 26897ms | 58509ms | 350 | all(lockfree) | 49ms | 414ms | 976ms | 5038ms | 10946ms | 351 | all(chan) | 83ms | 859ms | 3029ms | 19228ms | 40473ms | 352 353 354 #### 4.4. QR分布 355 356 快速率的分布情况如下所示: 357 358 | 类型 | 100 * 10000 | 500 * 10000 | 1000 * 10000 | 5000 * 10000 | 10000 * 10000 | 359 |---------------|-------------|-------------|--------------|--------------|---------------| 360 | lockfree | 99.23 | 99.78 | 99.81 | 99.49 | 98.99 | 361 | chan | 91.67 | 88.99 | 57.79 | 3.98 | 1.6 | 362 | all(lockfree) | 99.69 | 99.88 | 99.88 | 99.52 | 99.02 | 363 | all(chan) | 96.72 | 93.5 | 93.1 | 51.37 | 48.2 | 364 365 366 #### 4.5. 结果 367 368 从上面两张表可以很明显看出如下几点: 369 + 1)在goroutine数量比较小时,lockfree和chan性能差别不明显; 370 + 2)当goroutine打到一定数量(大于1000)后,lockfree无论从时间还是QR都远远超过chan;