本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
前言
前面两节(九)深入解析 Go 语言 GMP 模型:并发编程的核心机制 , (十)Go并发编程详解:锁、WaitGroup、Channel 我们讲完了Go并发的理论知识。
接下来我们需要在实战中使用,并体会这些并发原语的精髓,把握其中的细节,从而更加高效而合理的在我们的实际生产中使用他们,达到提升性能,保证并发安全的目的。
实战1 自增整数生成器
场景描述
自增整数生成器是一个常见的并发编程问题。我们需要一个能够生成唯一整数ID的生成器,并确保它在多线程环境下的线程安全性。
假设我们需要在一个分布式系统中生成全局唯一的任务ID:
- 唯一标识符生成器:在分布式系统中,每个节点需要生成唯一的ID,可以使用类似的自增整数生成器来保证唯一性。
go
package main
import (
"fmt"
"sync"
)
// Generator 结构体,包含一个互斥锁和一个任务ID计数器
type Generator struct {
mutex sync.Mutex
taskID int
}
// NewGenerator 创建一个新的 Generator
func NewGenerator() *Generator {
return &Generator{taskID: 0}
}
// Generate 以线程安全的方式生成一个新的唯一任务ID
func (g *Generator) Generate() int {
g.mutex.Lock()
defer g.mutex.Unlock()
g.taskID++
return g.taskID
}
// worker 函数,模拟一个工作线程,接收一个生成器和一个 WaitGroup
func worker(id int, gen *Generator, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
taskID := gen.Generate()
fmt.Printf("Worker %d 生成了任务ID: %d\n", id, taskID)
}
}
func main() {
gen := NewGenerator()
var wg sync.WaitGroup
// 启动3个工作线程,每个线程调用 worker 函数生成任务ID
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, gen, &wg)
}
// 使用 WaitGroup 确保所有goroutine完成后主程序才退出
wg.Wait()
}
代码分析
-
Generator结构体:
Generator
结构体包含一个sync.Mutex
和一个taskID
计数器。NewGenerator
函数用于创建一个新的Generator
实例。
-
Generate方法:
Generate
方法生成一个新的唯一任务ID,并通过加锁确保线程安全。- 使用
mutex.Lock()
加锁,然后将taskID
自增,并返回自增后的值。 - 使用
defer mutex.Unlock()
在函数退出时解锁,保证锁的释放。
-
worker函数:
worker
函数模拟一个工作线程,接收一个生成器和一个sync.WaitGroup
。- 在循环中调用
Generate
方法生成任务ID,并打印到标准输出。
-
main函数:
- 创建一个新的
Generator
实例。 - 启动3个工作线程,每个线程调用
worker
函数生成任务ID。 - 使用
sync.WaitGroup
确保所有goroutine完成后主程序才退出。
- 创建一个新的
场景总结
- 唯一标识符生成器:在分布式系统中,每个节点需要生成唯一的ID,可以使用类似的自增整数生成器来保证唯一性。
- 任务分发器:在任务调度系统中,需要为每个任务分配一个唯一的序号或者ID,可以使用此类生成器来实现。
- 数据流处理:在数据流处理中,有时候需要为数据流中的每个数据项分配唯一的标识符或者顺序号,此生成器也可以用来完成这个任务。
通过使用 sync.Mutex
进行加锁操作,我们确保了在并发环境下每次生成的任务ID都是唯一且递增的,解决了数据竞争的问题。
实战2 并发消息发送器
场景描述
并发消息发送器是一种用于在多线程环境下发送通知的机制。通过使用channel和goroutine,可以实现并发消息的发送。
假设我们需要实现一个并发的电子邮件发送系统,在用户注册时发送欢迎邮件:
go
package main
import (
"fmt"
"time"
)
func SendEmail(user string) <-chan string {
emailChannel := make(chan string, 1)
go func() {
defer close(emailChannel)
time.Sleep(2 * time.Second) // Simulate email sending delay
emailChannel <- fmt.Sprintf("Welcome email sent to %s", user)
}()
return emailChannel
}
func main() {
alice := SendEmail("alice")
bob := SendEmail("bob")
fmt.Println(<-alice)
fmt.Println(<-bob)
}
代码分析
-
SendEmail函数:
SendEmail
函数返回一个只接收数据的channel(<-chan string
)。- 创建了一个带缓冲的channel
emailChannel
,缓冲区大小为1。 - 在一个新的goroutine中,通过
time.Sleep
模拟发送电子邮件的延迟,生成欢迎邮件消息并发送到channelemailChannel
,然后关闭channel。
-
main函数:
- 在
main
函数中,调用SendEmail
函数分别为用户 "alice" 和 "bob" 生成消息发送器。 - 通过
<-alice
和<-bob
从各自的channel接收并打印邮件发送结果。
- 在
图示解释
- Main :主函数,调用
SendEmail
函数。 - SendEmail(alice) / SendEmail(bob) :为用户
alice
和bob
创建消息发送器。 - Goroutine(alice) / Goroutine(bob):在新goroutine中发送消息。
- emailChannel(alice) / emailChannel(bob):通过channel接收消息并返回给主函数。
这个图示展示了并发消息发送器的工作流程,通过goroutine和channel实现异步消息发送,使得主程序可以并发处理多个消息发送任务。
场景总结
这种并发的消息发送器常用于以下场景:
- 通知系统:在一个用户注册系统中,可以在用户注册成功后立即发送欢迎通知,确保通知发送过程不会阻塞主流程。
- 消息分发系统:在一个消息队列系统中,可以并发地将消息发送给多个订阅者,提高消息发送效率。
- 异步任务处理:在一个异步任务处理系统中,可以并发地处理多个任务,并将结果通过channel返回给调用方。
这个定时器例子展示了如何使用Go语言的goroutine和channel实现一个简单的定时器。这个定时器在指定的时间间隔后通过channel发送一个信号,可以用于在并发环境中处理超时操作。
实战3 定时器
场景描述
定时器是一个常见的并发编程模式,用于在一定时间后触发操作。通过使用channel和goroutine,可以实现一个简单的定时器。
假设我们需要实现一个网络请求超时处理系统:
go
package main
import (
"fmt"
"time"
)
func RequestHandler(timeout time.Duration) chan string {
result := make(chan string, 1)
go func() {
// 假设这里是一个耗时的请求
time.Sleep(3 * time.Second)
// 判断 result 是否已经关闭
select {
case <-result:
fmt.Println("Result channel is closed")
default:
result <- "Request successful"
}
}()
timer := Timer(timeout)
go func() {
select {
case res := <-result:
fmt.Println(res)
case <-timer:
fmt.Println("Request timed out")
}
close(result)
}()
return result
}
func Timer(duration time.Duration) chan bool {
ch := make(chan bool, 1)
go func() {
time.Sleep(duration)
ch <- true
}()
return ch
}
func main() {
RequestHandler(1 * time.Second)
time.Sleep(6 * time.Second) // 等待程序结束,查看打印结果
}
代码分析
-
Timer函数:
Timer
函数返回一个类型为chan bool
的channel。- 在一个新的goroutine中,使用
time.Sleep(duration)
延迟指定的时间,然后向channelch
发送true
信号。
-
RequestHandler函数:
RequestHandler
函数返回一个类型为chan string
的channel。- 在一个新的goroutine中模拟网络请求延迟,通过
time.Sleep(3 * time.Second)
延迟3秒后向channelresult
发送请求成功的消息。 - 创建一个定时器,超时时间为
timeout
。 - 在另一个新的goroutine中使用
select
语句等待result
或timer
的信号,如果result
有消息,则打印结果;如果timer
触发,则打印超时信息。这里的select是一个多通道监听器
-
main函数:
- 调用
RequestHandler
函数,并设置超时时间为5秒,查看输出。- 当我们将超时时间设置为1s时,我们再来看一下输出
- 使用
time.Sleep(6 * time.Second)
延迟主程序退出,确保可以看到输出。
- 调用
图示解释
- Main :主函数,调用
RequestHandler
函数。 - RequestHandler:处理网络请求和定时器。
- NetworkRequest :模拟网络请求延迟,向
ResultChannel
发送请求结果。 - Timer :定时器,向
TimeoutChannel
发送超时信号。 - ResultChannel:用于接收网络请求结果的channel。
- TimeoutChannel:用于接收超时信号的channel。
这个图示展示了并发请求处理系统的工作流程,通过goroutine和channel实现异步请求处理和超时控制,使得主程序可以并发处理多个请求并处理超时情况。
场景总结
这种定时器常用于以下场景:
- 任务超时处理:在网络请求或者任务处理中,如果某个任务在规定时间内没有完成,则触发超时操作。
- 定时任务:在某些情况下,需要定时执行某个任务或操作。
- 延迟执行:在某些情况下,需要延迟执行某个操作。
总结
通过以上几个实战例子,我们展示了如何在Go语言中实现并��编程。使用goroutine和channel,可以高效地处理并发任务,并确保线程安全性。
希望本篇文章能够帮助你更好地理解和掌握Go语言的并发编程。