自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

簡潔而不簡單的 sync.Once,你學會了嗎?

開發(fā) 前端
sync.Once? 的源代碼只有短短十幾行,看似簡單的條件分支背后充斥著 并發(fā)執(zhí)行?, 原子操作?, 同步原語 等基礎(chǔ)原理, 深入理解這些原理之后,可以幫助我們更好地構(gòu)建并發(fā)系統(tǒng),解決并發(fā)編程中遇到的問題。

概述

sync.Once? 可以保證在運行期間的某段程序只會執(zhí)行一次,典型的使用場景有 初始化配置?, 數(shù)據(jù)庫連接 等。

圖片

sync.Once 流程圖

與 init 函數(shù)差異

  • ? init 函數(shù)是當所在的 package 首次被加載時執(zhí)行,若遲遲未被使用,則既浪費了內(nèi)存,又延長了程序加載時間
  • ? sync.Once 方法可以在代碼的任意位置初始化和調(diào)用,并發(fā)場景下是線程安全的,因此可以延遲到使用時再調(diào)用 (懶加載)

示例

通過一個小例子展示 sync.Once 的使用方法。

package main

import (
"fmt"
"sync"
)


// 數(shù)據(jù)庫配置
type Config struct {
Server string
Port int
}

var (
once sync.Once
config *Config
)


// 初始化數(shù)據(jù)庫配置
func InitConfig() *Config {
once.Do(func() {
fmt.Println("mock init ...") // 模擬初始化代碼
})

return config
}

func main() {
// 連續(xù)調(diào)用 5 次初始化方法
for i := 0; i < 5; i++ {
_ = InitConfig()
}
}
$ go run main.go

# 輸出如下
mock init ...

從輸出的結(jié)果中可以看到,雖然我們調(diào)用了 5 次初始化配置方法,但是真正的初始化方法只執(zhí)行了 1 次,實現(xiàn)了設(shè)計模式中 單例模式 的效果。

圖片

方法調(diào)用結(jié)果

內(nèi)部實現(xiàn)

接下來,我們來探究一下 sync.Once? 的內(nèi)部實現(xiàn),文件路徑為 $GOROOT/src/sync/once.go?,筆者的 Go 版本為 go1.19 linux/amd64。

Once 結(jié)構(gòu)體

package sync

import (
"sync/atomic"
)

// Once 是一個只執(zhí)行一次操作的對象
// Once 一旦使用后,便不能再復(fù)制
//
// 在 Go 內(nèi)存模型術(shù)語中,once.Do(f) 中函數(shù) f 的返回值會在 once.Do() 函數(shù)返回前完成同步
type Once struct {
done uint32
m Mutex
}

sync.Once? 的結(jié)構(gòu)體有 2 個字段,m? 表示持有一個互斥鎖,這是并發(fā)調(diào)用場景下 只執(zhí)行一次? 的保證, done? 字段表示調(diào)用是否已完成,使用的字段類型是 uint32?, 這樣就可以使用標準庫中 atomic? 包里面 *Uint32 系列方法了,

為什么沒有使用 bool? 類型呢? 因為標準庫中 atomic? 包并未提供針對 bool? 類型的相關(guān)方法,如果適用 bool? 類型,操作時就需要轉(zhuǎn)換為 指針? 類型, 然后使用 atomic.*Pointer? 系列方法操作,這樣會造成內(nèi)存占用過多 (bool? 占用 1 個字節(jié),指針 占用 8 個字節(jié)) 和性能損耗 (參數(shù)類型轉(zhuǎn)換)。

done 字段

圖片

sync.Once 結(jié)構(gòu)體

done 作為結(jié)構(gòu)體的第一個字段,能夠減少 CPU 指令,也就是能夠提升性能,具體來說:

熱路徑 hot path? 是程序非常頻繁執(zhí)行的一系列指令,sync.Once? 絕大部分場景都會訪問 done? 字段,所以 done? 字段是處于 hot path? 上的,這樣一來 hot path 編譯后的機器碼指令更少,性能更高。

為什么放在第一個字段就能夠減少指令呢?因為結(jié)構(gòu)體第一個字段的地址和結(jié)構(gòu)體的指針是相同的,如果是第一個字段,直接對結(jié)構(gòu)體的指針解引用即可。如果是其他的字段,除了結(jié)構(gòu)體指針外,還需要計算與第一個值的 偏移量。在機器碼中,偏移量是隨指令傳遞的附加值,CPU 需要做一次偏移值與指針的加法運算, 才能獲取要訪問的值的地址,因此訪問第一個字段的機器碼更緊湊,速度更快。

Do 方法

// 當且僅當?shù)谝淮握{(diào)用實例 Once 的 Do 方法時,Do 去調(diào)用函數(shù) f
// 換句話說,調(diào)用 once.Do(f) 多次時,只有第一次調(diào)用會調(diào)用函數(shù) f,即使 f 函數(shù)在每次調(diào)用中有不同的參數(shù)值

// 并發(fā)調(diào)用 Do 函數(shù)時,需要等到其中的一個函數(shù) f 執(zhí)行之后才會返回
// 所以函數(shù) f 中不能調(diào)用同一個 once 實例的 Do 函數(shù) (遞歸調(diào)用),否則會發(fā)生死鎖
// 如果函數(shù) f 內(nèi)部 panic, Do 函數(shù)同樣認為其已經(jīng)返回,將來再次調(diào)用 Do 函數(shù)時,將不再執(zhí)行函數(shù) f
// 所以這就要求我們寫出健壯的 f 函數(shù)
func (o *Once) Do(f func()) {
// 下面是一個錯誤的實現(xiàn)
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }

// 錯誤原因分析:
// 這里以數(shù)據(jù)庫連接場景為例,在并發(fā)調(diào)用情況下,假設(shè)其中 1 個 goroutine 正在執(zhí)行函數(shù) f (初始化連接),
// 此時其他的 goroutine 將不會等待這個 goroutine 執(zhí)行完成,而是會直接返回,
// 如果連接發(fā)生了一些延遲,導(dǎo)致函數(shù) f 還未執(zhí)行完成,那么此時連接其實還未建立,
// 但是其他的 goroutine 認為函數(shù) f 已經(jīng)執(zhí)行完成,連接已建立,可以開始使用了
// 最后當其他 goroutine 使用未建立的連接操作時,產(chǎn)生報錯

// 要解決上面的問題, 就需要確保當前函數(shù)返回時, 函數(shù) f 已經(jīng)執(zhí)行完成,
// 這就是 slow path 退回到互斥鎖的原因,以及為什么 atomic.StoreUint32 需要延遲到函數(shù) f 返回之后
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f) // slow-path 允許內(nèi)聯(lián)
}
}

圖片

錯誤實現(xiàn)示例

doSlow 方法

func (o *Once) doSlow(f func()) {
// 并發(fā)場景下,可能會有多個 goroutine 執(zhí)行到這里
o.m.Lock() // 但是只有 1 個 goroutine 能獲取到互斥鎖
defer o.m.Unlock()

// 注意下面臨界區(qū)內(nèi)的判斷和修改

// 在 atomic.LoadUint32 時為 0 ,不等于獲取到鎖之后也是 0,所以需要二次檢測
// 因為已經(jīng)獲取到互斥鎖,根據(jù) Go 的同步原語約束,對于字段 done 的修改需要在獲取到互斥鎖之前同步
// 所以這里直接訪問字段即可,不需要調(diào)用 atomic.LoadUint32 方法
// 如果有其他 goroutine 已經(jīng)修改了字段 done,那么就不會進入條件分支,沒有任何影響
if o.done == 0 {
// 只要函數(shù) f 成功執(zhí)行過一次,就將 o.done 修改為 1
// 這樣其他 goroutine 就不會再執(zhí)行了,從而保證了函數(shù) f() 只會執(zhí)行一次,
// 這里必須使用 atomic.StoreUint32 方法來滿足 Go 的同步原語約束
defer atomic.StoreUint32(&o.done, 1)
f()
}
}

圖片

正確實現(xiàn)示例

小結(jié)

sync.Once? 的源代碼只有短短十幾行,看似簡單的條件分支背后充斥著 并發(fā)執(zhí)行?, 原子操作?, 同步原語 等基礎(chǔ)原理, 深入理解這些原理之后,可以幫助我們更好地構(gòu)建并發(fā)系統(tǒng),解決并發(fā)編程中遇到的問題。

Reference

  1. 1. Go sync.Once[1]

引用鏈接

[1]? Go sync.Once: https://geektutu.com/post/hpg-sync-once.html

責任編輯:武曉燕 來源: 洋芋編程
相關(guān)推薦

2024-06-05 11:06:22

Go語言工具

2021-08-29 18:13:03

緩存失效數(shù)據(jù)

2023-06-06 08:28:58

Sync.OnceGolang

2024-09-09 09:00:12

架構(gòu)設(shè)計算法

2022-07-08 09:27:48

CSSIFC模型

2024-02-02 11:03:11

React數(shù)據(jù)Ref

2023-01-10 08:43:15

定義DDD架構(gòu)

2024-02-04 00:00:00

Effect數(shù)據(jù)組件

2023-07-26 13:11:21

ChatGPT平臺工具

2024-01-19 08:25:38

死鎖Java通信

2024-01-02 12:05:26

Java并發(fā)編程

2023-08-01 12:51:18

WebGPT機器學習模型

2023-06-06 07:50:07

權(quán)限管理hdfsacl

2024-05-29 07:47:30

SpringJava@Resource

2022-12-06 08:37:43

2024-05-06 00:00:00

InnoDBView隔離

2024-08-06 09:47:57

2023-01-30 09:01:54

圖表指南圖形化

2024-07-31 08:39:45

Git命令暫存區(qū)

2023-12-12 08:02:10

點贊
收藏

51CTO技術(shù)棧公眾號