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

简介:

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

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

1 var once sync.Once
2  
3 once.Do(func() { fmt.Println("Once!") })

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

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

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

01 func onceDo() {
02     var num int
03     sign := make(chan bool)
04     var once sync.Once
05     f := func(ii int) func() {
06         return func() {
07             num = (num + ii*2)
08             sign <- true
09         }
10     }
11     for i := 0; i < 3; i++ {
12         fi := f(i + 1)
13         go once.Do(fi)
14     }
15     for j := 0; j < 3; j++ {
16         select {
17         case <-sign:
18             fmt.Println("Received a signal.")
19         case <-time.After(100 * time.Millisecond):
20             fmt.Println("Timeout!")
21         }
22     }
23     fmt.Printf("Num: %d.\n", num)
24 }

在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函数之后,我们会看到如下打印内容:

1 Received a signal.
2  
3 Timeout!
4  
5 Timeout!
6  
7 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类型及其方法实现了“只会执行一次”的语义。我们在需要完成只需或只能执行一次的任务的时候应该首先想到它。

相关文章
|
2月前
|
存储 算法 Go
Go语言实战:错误处理和panic_recover之自定义错误类型
本文深入探讨了Go语言中的错误处理和panic/recover机制,涵盖错误处理的基本概念、自定义错误类型的定义、panic和recover的工作原理及应用场景。通过具体代码示例介绍了如何定义自定义错误类型、检查和处理错误值,并使用panic和recover处理运行时错误。文章还讨论了错误处理在实际开发中的应用,如网络编程、文件操作和并发编程,并推荐了一些学习资源。最后展望了未来Go语言在错误处理方面的优化方向。
|
4月前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
5月前
|
存储 Go 开发者
Go语言中的并发编程与通道(Channel)的深度探索
本文旨在深入探讨Go语言中并发编程的核心概念和实践,特别是通道(Channel)的使用。通过分析Goroutines和Channels的基本工作原理,我们将了解如何在Go语言中高效地实现并行任务处理。本文不仅介绍了基础语法和用法,还深入讨论了高级特性如缓冲通道、选择性接收以及超时控制等,旨在为读者提供一个全面的并发编程视角。
138 50
|
5月前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####
|
5月前
|
算法 安全 程序员
Go语言的并发编程:深入理解与实践####
本文旨在探讨Go语言在并发编程方面的独特优势及其实现机制,通过实例解析关键概念如goroutine和channel,帮助开发者更高效地利用Go进行高性能软件开发。不同于传统的摘要概述,本文将以一个简短的故事开头,引出并发编程的重要性,随后详细阐述Go语言如何简化复杂并发任务的处理,最后通过实际案例展示其强大功能。 --- ###
|
5月前
|
Go 开发者
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念,重点介绍了goroutines和channels的工作原理及其在实际开发中的应用。文章通过实例演示如何有效地利用这些工具来编写高效、可维护的并发程序,旨在帮助读者理解并掌握Go语言在处理并发任务时的强大能力。 ####
|
5月前
|
安全 Go 数据处理
Go语言中的并发编程:掌握goroutine和channel的艺术####
本文深入探讨了Go语言在并发编程领域的核心概念——goroutine与channel。不同于传统的单线程执行模式,Go通过轻量级的goroutine实现了高效的并发处理,而channel作为goroutines之间通信的桥梁,确保了数据传递的安全性与高效性。文章首先简述了goroutine的基本特性及其创建方法,随后详细解析了channel的类型、操作以及它们如何协同工作以构建健壮的并发应用。此外,还介绍了select语句在多路复用中的应用,以及如何利用WaitGroup等待一组goroutine完成。最后,通过一个实际案例展示了如何在Go中设计并实现一个简单的并发程序,旨在帮助读者理解并掌
|
5月前
|
安全 Java Go
Go语言中的并发编程:掌握goroutine与通道的艺术####
本文深入探讨了Go语言中的核心特性——并发编程,通过实例解析goroutine和通道的高效使用技巧,旨在帮助开发者提升多线程程序的性能与可靠性。 ####
|
5月前
|
Go 调度 开发者
Go语言中的并发编程:深入理解goroutines和channels####
本文旨在探讨Go语言中并发编程的核心概念——goroutines和channels。通过分析它们的工作原理、使用场景以及最佳实践,帮助开发者更好地理解和运用这两种强大的工具来构建高效、可扩展的应用程序。文章还将涵盖一些常见的陷阱和解决方案,以确保在实际应用中能够避免潜在的问题。 ####
|
5月前
|
存储 安全 Go
Go 语言以其高效的并发编程能力著称,主要依赖于 goroutines 和 channels 两大核心机制
Go 语言以其高效的并发编程能力著称,主要依赖于 goroutines 和 channels 两大核心机制。本文介绍了这两者的概念、用法及如何结合使用,实现任务的高效并发执行与数据的安全传递,强调了并发编程中的注意事项,旨在帮助开发者更好地掌握 Go 语言的并发编程技巧。
67 2

热门文章

最新文章