GoLang的sync.WaitGroup与sync.Once简单使用讲解

 更新时间:2023年01月09日 11:07:31   作者:鲲鹏飞九万里  
sync.WaitGroup类型,它比通道更加适合实现这种一对多的goroutine协作流程。WaitGroup是开箱即用的,也是并发安全的。同时,与之前提到的同步工具一样,它一旦被真正的使用就不能被复制了

一、sync.WaitGroup的简单实用

在之前,我们使用通道,来主goroutine中等待其他goroutine执行完成:

func coordinateWithChan() {
	sign := make(chan struct{}, 2)
	num := int32(0)
	fmt.Printf("The number: %d [with chan struct{}]\n", num)
	max := int32(10)
	go addNum(&num, 1, max, func() {
		sign <- struct{}{}
	})
	go addNum(&num, 2, max, func() {
		sign <- struct{}{}
	})
	<-sign
	<-sign
}

其实,可以用更简单的方法,使用sync.WaitGroup来做:

func coordinateWithWaitGroup() {
	var wg sync.WaitGroup
	wg.Add(2)
	num := int32(0)
	fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)
	max := int32(10)
	go addNum(&num, 3, max, wg.Done)
	go addNum(&num, 4, max, wg.Done)
	wg.Wait()
}

sync包的WaitGroup类型。它比通道更加适合实现这种一对多的 goroutine 协作流程。

sync.WaitGroup类型(以下简称WaitGroup类型)是开箱即用的,也是并发安全的。同时,它一旦被真正使用就不能被复制了。

WaitGroup类型拥有三个指针方法:Add、Done和Wait。

Add方法

可以想象该类型中有一个计数器,它的默认值是0。我们可以通过调用该类型值的Add方法来增加,或者减少这个计数器的值。

Done方法

用这个方法来记录需要等待的 goroutine 的数量。相对应的,这个类型的Done方法,用于对其所属值中计数器的值进行减一操作。我们可以在需要等待的 goroutine 中,通过defer语句调用它。

Wait方法

此类型的Wait方法的功能是,阻塞当前的 goroutine,直到其所属值中的计数器归零。如果在该方法被调用的时候,那个计数器的值就是0,那么它将不会做任何事情。

二、sync.WaitGroup类型值中计数器的值可以小于0吗

不可以。

之所以说WaitGroup值中计数器的值不能小于0,是因为这样会引发一个 panic。 不适当地调用这类值的Done方法和Add方法都会如此。

  • 虽然WaitGroup值本身并不需要初始化,但是尽早地增加其计数器的值,还是非常有必要的。
  • WaitGroup值是可以被复用的,但需要保证其计数周期的完整性。
  • 不要把增加其计数器值的操作和调用其Wait方法的代码,放在不同的 goroutine 中执行。换句话说,要杜绝对同一个WaitGroup值的两种操作的并发执行。

三、sync.Once

sync.Once也属于结构体类型,同样也是开箱即用和并发安全的。由于这个类型包含了一个sync.Mutex类型的字段,所以,复制该类型的值也会导致功能的失效。

type Once struct {
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/386),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}

用法

Once类型的Do方法只接受一个参数,这个参数的类型必须是func(),即无参数声明和结果声明的函数。

该方法的功能并不是对每一种参数函数都只执行一次,而是只执行“首次被调用时传入的”那个函数,并且之后不会再执行任何参数函数。

package main
import (
	"fmt"
	"sync"
	"sync/atomic"
)
func main() {
	var counter uint32
	var once sync.Once
	once.Do(func() {
		atomic.AddUint32(&counter, 1)
	})
	fmt.Printf("The counter: %d\n", counter)
	once.Do(func() {
		atomic.AddUint32(&counter, 2)
	})
	fmt.Printf("The counter: %v\n", counter)
	fmt.Println()
}

$ go run demo02.go
The counter: 1
The counter: 1

$

所以,如果你有多个只需要执行一次的函数,那么就应该为它们中每一个都分配一个sync.Once类型的值。

sync.Once类型中的uint32类型的字段

sync.Once类型中有一个名叫done的uint32类型的字段。它的作用是记录其所属值的Do方法被调用的次数。该字段的值只可能为0或1。

一旦Do方法首次调用完成,它的值就会从0变为1。

使用uint32 类型是为了保证原子性。

修改done,使用了“双重判断+锁”的方式,类似于GoF设计模式中的单例模式。

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}
func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

Do方法的功能特点

第一个特点:于Do方法只会在参数函数执行结束之后把done字段的值变为1,因此,如果参数函数的执行需要很长时间或者根本就不会结束(比如执行一些守护任务),那么就有可能会导致相关 goroutine 的同时阻塞。

第二个特点:Do方法在参数函数执行结束后,对done字段的赋值用的是原子操作,并且,这一操作是被挂在defer语句中的。因此,不论参数函数的执行会以怎样的方式结束,done字段的值都会变为1。

也就是说,即使这个参数函数没有执行成功(比如引发了一个 panic),我们也无法使用同一个Once值重新执行它了。所以,如果你需要为参数函数的执行设定重试机制,那么就要考虑Once值的适时替换问题。

到此这篇关于GoLang的sync.WaitGroup与sync.Once简单使用讲解的文章就介绍到这了,更多相关Go sync.WaitGroup与sync.Once内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang设计模式之外观模式的实现

    Golang设计模式之外观模式的实现

    这篇文章主要介绍了Golang设计模式之外观模式的实现,外观模式是一种常用的设计模式之一,是一种结构型设计模式,它提供了一个简单的接口来访问复杂系统的各种功能,从而降低了系统的复杂度,需要详细了解可以参考下文
    2023-05-05
  • Golang try catch与错误处理的实现

    Golang try catch与错误处理的实现

    社区不少人在谈论 golang 为毛不用try/catch模式,而采用苛刻的recovery、panic、defer组合,本文就来详细的介绍一下,感兴趣的可以了解一下
    2021-07-07
  • Go uuid库的具体使用

    Go uuid库的具体使用

    在现代软件开发中,全球唯一标识符(UUID)在许多场景中发挥着重要的作用,本文主要介绍了Go uuid库的具体使用,具有一定的参考价值,感兴趣的可以了解一下
    2023-11-11
  • Go时间操作常用方法(推荐!)

    Go时间操作常用方法(推荐!)

    平时开发过程中,时间相关的操作用的还是很多的,下面这篇文章主要给大家介绍了关于Go时间操作常用方法的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-06-06
  • 详解Go语言如何检查系统命令是否可用

    详解Go语言如何检查系统命令是否可用

    这篇文章主要为大家详细介绍了Go语言通过编写一个函数,利用Go语言标准库中的功能来检查系统命令是否可用,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-01-01
  • Golang中的Interface详解

    Golang中的Interface详解

    本文详细讲解了Golang中的Interface,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • golang语法使用的注意事项

    golang语法使用的注意事项

    这篇文章主要给大家介绍了关于golang语法使用的一些注意事项,Golang是一种静态类型的编程语言,它支持基本的数据类型,包括整型、浮点型、布尔型、字符串型,需要的朋友可以参考下
    2023-07-07
  • Golang爬虫框架 colly的使用

    Golang爬虫框架 colly的使用

    本文主要介绍了Golang爬虫框架 colly的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • Go目录文件路径操作的实现

    Go目录文件路径操作的实现

    在Go语言中,可以使用绝对路径或相对路径来表示文件路径,本文就来介绍一下Go目录文件路径操作,感兴趣的可以了解一下
    2023-10-10
  • Go语言学习之运算符使用详解

    Go语言学习之运算符使用详解

    这篇文章主要介绍了Go语言中常用运算符的使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04

最新评论