《GO并发编程实战》—— 只会执行一次

简介:

声明:本文是《Go并发编程实战》的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文。

现在,让我们再次聚焦到sync代码包。除了我们介绍过的互斥锁、读写锁和条件变量,该代码包还为我们提供了几个非常有用的API。其中一个比较有特色的就是结构体类型sync.Once和它的Do方法。

与代表锁的结构体类型sync.Mutex和sync.RWMutex一样,sync.Once也是开箱即用的。换句话说,我们仅需对它进行简单的声明即可使用,就像这样:


var once sync.Once

once.Do(func() { fmt.Println("Once!") })


如上所示,我们声明了一个名为once的sync.Once类型的变量之后,立刻就可以调用它的指针方法Do了。

该类型的方法Do可以接受一个无参数、无结果的函数值作为其参数。该方法一旦被调用,就会调用被作为参数传入的那个函数。从这一点看,该方法的功能实在是稀松平常。不过,重点并不在这里。

我们对一个sync.Once类型值的指针方法Do的有效调用次数永远会是1。也就是说,无论我们调用这个方法多少次,也无论我们在多次调用时传递给它的参数值是否相同,都仅有第一次调用是有效的。无论怎样,只有我们第一次调用该方法时传递给它的那个函数会被执行。请看下面的示例:


func onceDo() {
    var num int
    sign := make(chan bool)
    var once sync.Once
    f := func(ii int) func() {
        return func() {
            num = (num + ii*2)
            sign <- true
        }
    }
    for i := 0; i < 3; i++ {
        fi := f(i + 1)
        go once.Do(fi)
    }
    for j := 0; j < 3; j++ {
        select {
        case <-sign:
            fmt.Println("Received a signal.")
        case <-time.After(100 * time.Millisecond):
            fmt.Println("Timeout!")
        }
    }
    fmt.Printf("Num: %d.\n", num)
}


在onceDo函数中,我们利用for语句连续三次异步的调用once变量的Do方法。这三次调用传给Do方法的参数值都是相同的,都是变量fi所代表的匿名函数值。这个函数值的功能是先改变num变量的值再向非缓冲的sign通道发送一个true。变量num的值可以表示出once的Do方法被有效调用的次数,而通道sign则被用来传递代表了fi函数被执行完毕的信号。请注意,为了能够精确的表达出fi函数是在哪一次(或哪几次)调用once.Do方法的时候被执行的,我们在这里使用了闭包。在每次迭代之初,我们赋给fi变量的函数值都是对变量f所代表的函数值进行闭包的一个结果值。我们使用变量ii作为f函数中的自由变量,并在闭包的过程中把for代码块中的变量i的值加1后再与该自由变量绑定在一起。这样就生成了为当次迭代专门定制的函数fi。每次迭代中生成的fi函数在被执行的时候都会修改变量num的值。这些新的值不会出现重复,并且非常有助于我们倒推出所有的曾赋给自由变量的ii的值。这样,我们就可以知道哪个(或哪些)fi函数被真正的执行了。

函数onceDo中的第二条for语句的作用是等待之前的那三个异步调用的完成。读者可能已经发现,这两条for语句的预设迭代次数是一致的。在第二条for语句中,我们使用了select语句,并且为针对sign通道的接收操作设定了超时时间(100毫秒)。这是为了当永远无法从sign通道中接收元素值的时候不至于造成永久的阻塞。select语句中的每个case在被执行时都会打印出相应的内容。这有助于我们观察程序的实际运行情况。最后,我们还会打印出num变量的值。据此,我们可以判断在前面几次传递给Do方法的fi是否都被执行了。

在执行onceDo函数之后,我们会看到如下打印内容:


Received a signal.

Timeout!

Timeout!

Num: 2.


上面的打印内容表明,在成功从sign通道接收了一个元素值之后,出现了两次接收操作超时的情况。我们不用考虑在对sign通道的接收操作开始之时匿名函数fi还没有被执行完毕的情况。因为100毫秒的时间已经足够执行它很多很多次的了。因此,这两次接收操作超时应该是当时没有正在为此等待的对sign通道的发送操作导致的(注意,sign是一个非缓冲通道)。综上所述,我们可以初步判断,传递给once.Do方法的匿名函数fi只被执行了一次。并且,这仅有一次的执行的对象是在我们第一次调用该方法时传递给它的那个fi函数。

依据最后一行打印内容,我们可以证实上述判断。num变量的值为2意味着它只被修改了一次,并且是在自由变量ii为1的时候被修改的。这就可以证实,只有在for循环的第一次迭代时传递给once.Do方法的那个fi函数被执行了。这也符合sync.Once类型及其指针方法Do的语义。

请注意,这个仅被执行一次的限制只是针对单个sync.Once类型值来说的。换句话说,每个sync.Once类型值的指针方法Do都可以被有效的调用一次。

这个sync.Once类型的典型应用场景就是执行仅需执行一次的任务。例如,数据库连接池的初始化任务。又例如,一些心跳检测之类的实时监测任务。等等。

在一探sync.Once类型及其指针方法Do的内部实现之后,我们会有所发现:它们所提供的功能正是由前面讲到的互斥锁和原子操作来实现的。这个实现并不复杂。其使用的技巧包括卫述语句、双重检查锁定,以及对共享标记的原子读写操作。在熟知了本章讲述的这些同步工具之后,我们是否也能轻易设计出这样简单、有效的解决方案呢?

总之,sync.Once类型及其方法实现了“只会执行一次”的语义。我们在需要完成只需或只能执行一次的任务的时候应该首先想到它。 

目录
相关文章
|
14天前
|
Go 调度 开发者
Go语言中的并发编程:深入理解与实践###
探索Go语言在并发编程中的独特优势,揭秘其高效实现的底层机制。本文通过实例和分析,引导读者从基础到进阶,掌握Goroutines、Channels等核心概念,提升并发处理能力。 ###
|
2月前
|
Shell Go API
Go语言grequests库并发请求的实战案例
Go语言grequests库并发请求的实战案例
|
2天前
|
Serverless Go
Go语言中的并发编程:从入门到精通
本文将深入探讨Go语言中并发编程的核心概念和实践,包括goroutine、channel以及sync包等。通过实例演示如何利用这些工具实现高效的并发处理,同时避免常见的陷阱和错误。
|
3天前
|
安全 Go 开发者
代码之美:Go语言并发编程的优雅实现与案例分析
【10月更文挑战第28天】Go语言自2009年发布以来,凭借简洁的语法、高效的性能和原生的并发支持,赢得了众多开发者的青睐。本文通过两个案例,分别展示了如何使用goroutine和channel实现并发下载网页和构建并发Web服务器,深入探讨了Go语言并发编程的优雅实现。
10 2
|
5天前
|
Go 调度 开发者
Go语言的并发编程模型
【10月更文挑战第26天】Go语言的并发编程模型
8 1
|
9天前
|
安全 Go 调度
Go语言中的并发编程:解锁高性能程序设计之门####
探索Go语言如何以简洁高效的并发模型,重新定义现代软件开发的边界。本文将深入剖析Goroutines与Channels的工作原理,揭秘它们为何成为实现高并发、高性能应用的关键。跟随我们的旅程,从基础概念到实战技巧,一步步揭开Go并发编程的神秘面纱,让您的代码在多核时代翩翩起舞。 ####
|
11天前
|
存储 Go 开发者
Go语言中的并发编程与通道机制
本文将探讨Go语言中并发编程的核心概念——goroutine和通道(channel)。我们将从基础开始,解释什么是goroutine以及如何创建和使用它们。然后,我们将深入探讨通道的概念、类型以及如何使用通道在goroutine之间进行通信。最后,我们将通过一个示例来展示如何在实际应用中使用goroutine和通道来实现并发编程。
|
17天前
|
Go 开发者
Go语言中的并发编程:从基础到实践
在当今的软件开发中,并发编程已经成为了一项不可或缺的技能。Go语言以其简洁的语法和强大的并发支持,成为了开发者们的首选。本文将带你深入了解Go语言中的并发编程,从基础概念到实际应用,帮助你掌握这一重要的编程技能。
|
4天前
|
Go
Go语言中的并发编程:深入探索与实践###
探索Go语言的并发编程,就像解锁了一把高效处理复杂任务的钥匙。本文旨在通过简明扼要的方式,阐述Goroutines和Channels如何协同工作,以实现高效的并发处理。不同于传统的技术文档,这里我们将用一个生动的故事来串联起这些概念,让你在轻松阅读中领悟到并发编程的精髓。 ###
|
21天前
|
负载均衡 安全 物联网
探索Go语言的并发编程模型及其在现代应用中的优势
【10月更文挑战第10天】探索Go语言的并发编程模型及其在现代应用中的优势