Go并发编程之sync.Once独霸实例详解

s7712026-07-20 19:25:58

一.序

单从库名大年夜大年夜体就可以猜出其感染 。并发编程霸实sync.Once独霸起来很复杂,例详 上面是一个复杂的独霸案例

package main import (	"fmt"	"sync") func main() { 	var (		once sync.Once		wg   sync.WaitGroup	) 	for i := 0; i < 10; i++ { 		wg.Add(1)		// 这里要寄看讲i展示的当参数传进内部的匿名函数		go func(i int) { 			defer wg.Done()			// fmt.Println("once", i)			once.Do(func() { 				fmt.Println("once", i)			})		}(i)	} 	wg.Wait()	fmt.Printf("over")}

输进 :

❯ go run ./demo.go
once 9

测试假定不添加once.Do 这段代码  ,则会输进以下下场 ,并发编程霸实并且每次奉行的例详输进都不一样。

once 9
once 0
once 3
once 6
once 4
once 1
once 5
once 2
once 7
once 8

从两次输进不合 ,并发编程霸实我们可以得知 sync.Once的例详感染是 :担保传进的函数只奉行一次

二. 源码分化

2.1筹划体

Once的筹划体以下

type Once struct {     done uint32    m    Mutex}

每个 sync.Once 筹划体中都只包含一个用于标识代码块可否奉行过的 done 和一个互斥锁 sync.Mutex

2.2 接口

sync.Once.Do 是 sync.Once 筹划体对外独一泄漏的编制,该编制会领受一个进参为空的并发编程霸实函数 :

  • 假定传进的函数已奉行过  ,会直接前去
  • 假定传进的例详函数没有奉行过 , 会调用sync.Once.doSlow奉行传进的并发编程霸实参数
func (o *Once) Do(f func()) { 	// Note: Here is an incorrect implementation of Do:	//	//	if atomic.CompareAndSwapUint32(&o.done, 0, 1) { 	//		f()	//	}	//	// Do guarantees that when it returns, f has finished.	// This implementation would not implement that guarantee:	// given two simultaneous calls, the winner of the cas would	// call f, and the second would return immediately, without	// waiting for the first's call to f to complete.	// This is why the slow path falls back to a mutex, and why	// the atomic.StoreUint32 must be delayed until after f returns. 	if atomic.LoadUint32(&o.done) == 0 { 		// Outlined slow-path to allow inlining of the fast-path.		o.doSlow(f)	}}

代码诠释中出格给了一个声明: 很随便犯错的一种完成

if atomic.CompareAndSwapUint32(&o.done, 0, 1) { 	f()}

假定这么完成最除夜的问题是 ,假定并发调用,例详一个 goroutine 奉行 ,并发编程霸实此外一个不会等正在奉行的例详这个成功此后前去 ,而是并发编程霸实直接就前去了,这就不克不及担保传进的例详编制必定会先奉行一次了

切确的完成编制

if atomic.LoadUint32(&o.done) == 0 {     // Outlined slow-path to allow inlining of the fast-path.    o.doSlow(f)}

会先剖断 done 可否为 0 ,假定不为 0 声明还没奉行过 ,并发编程霸实就进进 doSlow

func (o *Once) doSlow(f func()) { 	o.m.Lock()	defer o.m.Unlock()	if o.done == 0 { 		defer atomic.StoreUint32(&o.done, 1)		f()	}}

在 doSlow 傍边独霸了互斥锁来担保只会奉行一次

具体的逻辑

  • 为往后Goroutine掉落踪掉落踪互斥锁
  • 奉行传进的无进参函数;
  • 运转迟误函数, 将成员变量done更新为1

三. 独霸处景案例

3.1 单例编制

原子独霸合营互斥锁可以完成特别很是高效的单件编制 。互斥锁的价值比深化整数的原子读写高良多 ,在功用敏感的中心可以添加一个数字型的标识表记标帜位,经由过程原子检测标识表记标帜位外形降低互斥锁的独霸次数来进步功用 。

type singleton struct { } var (    instance    *singleton    initialized uint32    mu          sync.Mutex) func Instance() *singleton {     if atomic.LoadUint32(&initialized) == 1 {         return instance    }     mu.Lock()    defer mu.Unlock()     if instance == nil {         defer atomic.StoreUint32(&initialized, 1)        instance = &singleton{ }    }    return instance}

而独霸sync.Once能更复杂完成单例编制

type singleton struct { } var (    instance *singleton    once     sync.Once) func Instance() *singleton {     once.Do(func() {         instance = &singleton{ }    })    return instance}

3.2 加载设置设备放置文件示例

迟误一个开消很除夜的初始化独霸到真正用到它的时辰再奉行是一个很好的幻想 。因为过后初始化一个变量(比如在init函数中完成初始化)会添加法度圭表类型的启动耗时 ,并且有大年夜大年夜约理论奉行过程中这个变量没有效上 ,那么这个初始化独霸就不是必须求做的  。我们来看一个例子:

var icons map[string]image.Image func loadIcons() {     icons = map[string]image.Image{         "left":  loadIcon("left.png"),        "up":    loadIcon("up.png"),        "right": loadIcon("right.png"),        "down":  loadIcon("down.png"),    }} // Icon 被多个goroutine调用时不是并发安然的// 因为map圭表类型本就不是圭表类型安然数据筹划func Icon(name string) image.Image {     if icons == nil {         loadIcons()    }    return icons[name]}

多个goroutine并发调用Icon函数时不是并发安然的,编译器和CPU大年夜大年夜约会在担保每个goroutine都知足串行齐截的根本上逍遥地重排访谒内存的按序 。loadIcons函数大年夜大年夜约会被重排为以下下场 :

func loadIcons() {
    icons = make(map[string]image.Image)
    icons["left"] = loadIcon("left.png")
    icons["up"] = loadIcon("up.png")
    icons["right"] = loadIcon("right.png")
    icons["down"] = loadIcon("down.png")
}

在这类气候下就会展示即便剖断了icons不是nil也不虞味着变量初始化完成了 。思虑到这类气候,我们能想到的编制就是添加互斥锁,担保初始化icons的时辰不会被其他的goroutine独霸 ,可是多么做又会激起功用问题。

可独霸sync.Once 更始代码

var icons map[string]image.Image var loadIconsOnce sync.Once func loadIcons() {     icons = map[string]image.Image{         "left":  loadIcon("left.png"),        "up":    loadIcon("up.png"),        "right": loadIcon("right.png"),        "down":  loadIcon("down.png"),    }} // Icon 是并发安然的,并且担保了在代码运转的时辰才会加载设置设备放置func Icon(name string) image.Image {     loadIconsOnce.Do(loadIcons)    return icons[name]}

多么筹划就可以担保初始化独霸的时辰是并发安然的并且初始化独霸也不会被奉行多次。

四.总结

作为用于担保函数奉行次数的 sync.Once 筹划体,它独霸互斥锁和 sync/atomic 包供给的编制完成了某个函数在法度圭表类型运转时代只能奉行一次的语义。在独霸该筹划体时  ,我们也需求寄看以下的问题 :

  • sync.Once.Do 编制中传进的函数只会被奉行一次 ,哪怕函数中产生发火了 panic;
  • 两次调用 sync.Once.Do 编制传进不合的函数只会奉行第一次调传进的函数;

五. 参考

  • https://lailin.xyz/post/go-training-week3-once.html
  • https://www.topgoer.cn/docs/gozhuanjia/chapter055.2-waitgroup
  • https://www.topgoer.com/并发编程/sync.html
  • https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html

到此这篇关于Go

一.序

单从库名大年夜大年夜体就可以猜出其感染 。sync.Once独霸起来很复杂, 上面是一个复杂的独霸案例

package main import (	"fmt"	"sync") func main() { 	var (		once sync.Once		wg   sync.WaitGroup	) 	for i := 0; i < 10; i++ { 		wg.Add(1)		// 这里要寄看讲i展示的当参数传进内部的匿名函数		go func(i int) { 			defer wg.Done()			// fmt.Println("once", i)			once.Do(func() { 				fmt.Println("once", i)			})		}(i)	} 	wg.Wait()	fmt.Printf("over")}

输进:

❯ go run ./demo.go
once 9

测试假定不添加once.Do 这段代码 ,则会输进以下下场 ,并且每次奉行的输进都不一样 。

once 9
once 0
once 3
once 6
once 4
once 1
once 5
once 2
once 7
once 8

从两次输进不合,我们可以得知 sync.Once的感染是:担保传进的函数只奉行一次

二. 源码分化

2.1筹划体

Once的筹划体以下

type Once struct {     done uint32    m    Mutex}

每个 sync.Once 筹划体中都只包含一个用于标识代码块可否奉行过的 done 和一个互斥锁 sync.Mutex

2.2 接口

sync.Once.Do 是 sync.Once 筹划体对外独一泄漏的编制 ,该编制会领受一个进参为空的函数 :

  • 假定传进的函数已奉行过,会直接前去
  • 假定传进的函数没有奉行过, 会调用sync.Once.doSlow奉行传进的参数
func (o *Once) Do(f func()) { 	// Note: Here is an incorrect implementation of Do:	//	//	if atomic.CompareAndSwapUint32(&o.done, 0, 1) { 	//		f()	//	}	//	// Do guarantees that when it returns, f has finished.	// This implementation would not implement that guarantee:	// given two simultaneous calls, the winner of the cas would	// call f, and the second would return immediately, without	// waiting for the first's call to f to complete.	// This is why the slow path falls back to a mutex, and why	// the atomic.StoreUint32 must be delayed until after f returns. 	if atomic.LoadUint32(&o.done) == 0 { 		// Outlined slow-path to allow inlining of the fast-path.		o.doSlow(f)	}}

代码诠释中出格给了一个声明: 很随便犯错的一种完成

if atomic.CompareAndSwapUint32(&o.done, 0, 1) { 	f()}

假定这么完成最除夜的问题是 ,假定并发调用,一个 goroutine 奉行 ,此外一个不会等正在奉行的这个成功此后前去,而是直接就前去了,这就不克不及担保传进的编制必定会先奉行一次了

切确的完成编制

if atomic.LoadUint32(&o.done) == 0 {     // Outlined slow-path to allow inlining of the fast-path.    o.doSlow(f)}

会先剖断 done 可否为 0,假定不为 0 声明还没奉行过,就进进 doSlow

func (o *Once) doSlow(f func()) { 	o.m.Lock()	defer o.m.Unlock()	if o.done == 0 { 		defer atomic.StoreUint32(&o.done, 1)		f()	}}

在 doSlow 傍边独霸了互斥锁来担保只会奉行一次

具体的逻辑

  • 为往后Goroutine掉落踪掉落踪互斥锁
  • 奉行传进的无进参函数;
  • 运转迟误函数 , 将成员变量done更新为1

三. 独霸处景案例

3.1 单例编制

原子独霸合营互斥锁可以完成特别很是高效的单件编制。互斥锁的价值比深化整数的原子读写高良多,在功用敏感的中心可以添加一个数字型的标识表记标帜位,经由过程原子检测标识表记标帜位外形降低互斥锁的独霸次数来进步功用 。

type singleton struct { } var (    instance    *singleton    initialized uint32    mu          sync.Mutex) func Instance() *singleton {     if atomic.LoadUint32(&initialized) == 1 {         return instance    }     mu.Lock()    defer mu.Unlock()     if instance == nil {         defer atomic.StoreUint32(&initialized, 1)        instance = &singleton{ }    }    return instance}

而独霸sync.Once能更复杂完成单例编制

type singleton struct { } var (    instance *singleton    once     sync.Once) func Instance() *singleton {     once.Do(func() {         instance = &singleton{ }    })    return instance}

3.2 加载设置设备放置文件示例

迟误一个开消很除夜的初始化独霸到真正用到它的时辰再奉行是一个很好的幻想。因为过后初始化一个变量(比如在init函数中完成初始化)会添加法度圭表类型的启动耗时 ,并且有大年夜大年夜约理论奉行过程中这个变量没有效上,那么这个初始化独霸就不是必须求做的 。我们来看一个例子:

var icons map[string]image.Image func loadIcons() {     icons = map[string]image.Image{         "left":  loadIcon("left.png"),        "up":    loadIcon("up.png"),        "right": loadIcon("right.png"),        "down":  loadIcon("down.png"),    }} // Icon 被多个goroutine调用时不是并发安然的// 因为map圭表类型本就不是圭表类型安然数据筹划func Icon(name string) image.Image {     if icons == nil {         loadIcons()    }    return icons[name]}

多个goroutine并发调用Icon函数时不是并发安然的 ,编译器和CPU大年夜大年夜约会在担保每个goroutine都知足串行齐截的根本上逍遥地重排访谒内存的按序 。loadIcons函数大年夜大年夜约会被重排为以下下场 :

func loadIcons() {
    icons = make(map[string]image.Image)
    icons["left"] = loadIcon("left.png")
    icons["up"] = loadIcon("up.png")
    icons["right"] = loadIcon("right.png")
    icons["down"] = loadIcon("down.png")
}

在这类气候下就会展示即便剖断了icons不是nil也不虞味着变量初始化完成了。思虑到这类气候,我们能想到的编制就是添加互斥锁,担保初始化icons的时辰不会被其他的goroutine独霸 ,可是多么做又会激起功用问题 。

可独霸sync.Once 更始代码

var icons map[string]image.Image var loadIconsOnce sync.Once func loadIcons() {     icons = map[string]image.Image{         "left":  loadIcon("left.png"),        "up":    loadIcon("up.png"),        "right": loadIcon("right.png"),        "down":  loadIcon("down.png"),    }} // Icon 是并发安然的,并且担保了在代码运转的时辰才会加载设置设备放置func Icon(name string) image.Image {     loadIconsOnce.Do(loadIcons)    return icons[name]}

多么筹划就可以担保初始化独霸的时辰是并发安然的并且初始化独霸也不会被奉行多次 。

四.总结

作为用于担保函数奉行次数的 sync.Once 筹划体 ,它独霸互斥锁和 sync/atomic 包供给的编制完成了某个函数在法度圭表类型运转时代只能奉行一次的语义 。在独霸该筹划体时 ,我们也需求寄看以下的问题:

  • sync.Once.Do 编制中传进的函数只会被奉行一次 ,哪怕函数中产生发火了 panic;
  • 两次调用 sync.Once.Do 编制传进不合的函数只会奉行第一次调传进的函数;

五. 参考

  • https://lailin.xyz/post/go-training-week3-once.html
  • https://www.topgoer.cn/docs/gozhuanjia/chapter055.2-waitgroup
  • https://www.topgoer.com/并发编程/sync.html
  • https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html

到此这篇关于GoGo言语

上一篇:奥拉星手游躲灵一星灵推荐
下一篇:复杂修个仙法修速通流攻略
相关文章