《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类型及其方法实现了“只会执行一次”的语义。我们在需要完成只需或只能执行一次的任务的时候应该首先想到它。

相关文章
|
4月前
|
Linux Go iOS开发
Go语言100个实战案例-进阶与部署篇:使用Go打包生成可执行文件
本文详解Go语言打包与跨平台编译技巧,涵盖`go build`命令、多平台构建、二进制优化及资源嵌入(embed),助你将项目编译为无依赖的独立可执行文件,轻松实现高效分发与部署。
|
5月前
|
数据采集 数据挖掘 测试技术
Go与Python爬虫实战对比:从开发效率到性能瓶颈的深度解析
本文对比了Python与Go在爬虫开发中的特点。Python凭借Scrapy等框架在开发效率和易用性上占优,适合快速开发与中小型项目;而Go凭借高并发和高性能优势,适用于大规模、长期运行的爬虫服务。文章通过代码示例和性能测试,分析了两者在并发能力、错误处理、部署维护等方面的差异,并探讨了未来融合发展的趋势。
459 0
|
5月前
|
存储 人工智能 Go
Go-Zero全流程实战即时通讯
Go-Zero 是一个功能丰富的微服务框架,适用于开发高性能的即时通讯应用。它具备中间件、工具库和代码生成器,简化开发流程。本文介绍其环境搭建、项目初始化及即时通讯功能实现,涵盖用户认证、消息收发和实时推送,帮助开发者快速上手。
399 0
|
4月前
|
存储 前端开发 JavaScript
Go语言实战案例-项目实战篇:编写一个轻量级在线聊天室
本文介绍如何用Go语言从零实现一个轻量级在线聊天室,基于WebSocket实现实时通信,支持多人消息广播。涵盖前后端开发、技术选型与功能扩展,助你掌握Go高并发与实时通信核心技术。
|
5月前
|
负载均衡 监控 Java
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
在微服务架构中,高可用与稳定性至关重要。本文详解熔断、限流与负载均衡三大关键技术,结合API网关与Hystrix-Go实战,帮助构建健壮、弹性的微服务系统。
612 1
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
|
5月前
|
安全 Go 开发者
Go语言实战案例:使用sync.Mutex实现资源加锁
在Go语言并发编程中,数据共享可能导致竞态条件,使用 `sync.Mutex` 可以有效避免这一问题。本文详细介绍了互斥锁的基本概念、加锁原理及实战应用,通过构建并发安全的计数器演示了加锁与未加锁的区别,并封装了一个线程安全的计数器结构。同时对比了Go中常见的同步机制,帮助开发者理解何时应使用 `Mutex` 及其注意事项。掌握 `Mutex` 是实现高效、安全并发编程的重要基础。
|
5月前
|
数据采集 Go API
Go语言实战案例:使用context控制协程取消
本文详解 Go 语言中 `context` 包的使用,通过实际案例演示如何利用 `context` 控制协程的生命周期,实现任务取消、超时控制及优雅退出,提升并发程序的稳定性与资源管理能力。
|
5月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
5月前
|
Go 开发者
Go语言实战案例:使用select监听多个channel
本文为《Go语言100个实战案例 · 网络与并发篇》第5篇,详解Go并发核心工具`select`的使用。通过实际案例讲解如何监听多个Channel、实现多任务处理、超时控制和非阻塞通信,帮助开发者掌握Go并发编程中的多路异步事件处理技巧。
|
5月前
|
数据采集 编解码 监控
Go语言实战案例:使用channel实现生产者消费者模型
本文是「Go语言100个实战案例 · 网络与并发篇」第4篇,通过实战案例详解使用 Channel 实现生产者-消费者模型,涵盖并发控制、任务调度及Go语言并发哲学,助你掌握优雅的并发编程技巧。