并发是现代软件开发中的一个重要概念,它允许程序同时执行多个任务,提高系统的性能和响应能力。Golang 是一门天生支持并发的语言,它通过 goroutine 和 channel 提供了强大的并发编程支持。
在本文中,我们将深入探讨 Golang 中的并发编程,了解 goroutine、channel 以及一些常见的并发模式。
Goroutine 是 Golang 中的轻量级线程,由 Go 运行时系统调度。它们比传统的线程更轻便,创建和销毁的代价更小,使得在程序中创建成千上万个 goroutine 变得实际可行。
在 Golang 中创建 goroutine 非常简单。只需在函数调用前使用关键字 go
就可以将其包装成 goroutine。
package main
import (
"fmt"
"sync"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func main() {
go printNumbers()
// 主 goroutine 不等待 printNumbers 完成
time.Sleep(1 * time.Second)
fmt.Println("Main goroutine")
}
上述代码中,printNumbers
函数被包装成一个 goroutine,并在 main
函数中异步执行。time.Sleep(1 * time.Second)
用于等待足够的时间,确保 printNumbers
有足够的时间打印数字。注意,主 goroutine 并不会等待 printNumbers
执行完毕。
Channel 是 Golang 中用于在 goroutine 之间传递数据的管道。它提供了一种同步的方式,确保数据安全地在 goroutine 之间传递。
在 Golang 中,使用 make
函数创建 channel。
ch := make(chan int)
上述代码创建了一个整数类型的无缓冲 channel。无缓冲 channel 在发送数据和接收数据时都会阻塞,直到另一方准备好。
package main
import (
"fmt"
"sync"
"time"
)
func printNumbers(ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go printNumbers(ch, &wg)
for num := range ch {
fmt.Printf("%d ", num)
}
fmt.Println("Main goroutine")
wg.Wait()
}
上述代码中,printNumbers
将数字通过 channel 发送给主 goroutine,并在发送完毕后关闭 channel。主 goroutine 使用 for num := range ch
循环接收 channel 中的数据,直到 channel 被关闭。同时,使用等待组(sync.WaitGroup
)确保所有 goroutine 执行完毕后主 goroutine 才结束。
除了简单的 goroutine 和 channel 的使用外,Golang 还提供了一些常见的并发模式,如等待组、互斥锁等。这些模式帮助我们更好地组织和管理并发代码。
等待组(WaitGroup)用于等待一组 goroutine 执行完毕。它通过计数器实现,每个 goroutine 执行前递增计数器,执行完毕后递减计数器。主 goroutine 调用 Wait
方法等待计数器归零。
package main
import (
"fmt"
"sync"
"time"
)
func printNumbers(wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go printNumbers(&wg)
wg.Wait()
fmt.Println("Main goroutine")
}
互斥锁(Mutex)用于保护共享资源,防止多个 goroutine 同时访问导致的竞态条件。
package main
import (
"fmt"
"sync"
"time"
)
var counter = 0
var mutex sync.Mutex
func increment(wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 1000; i++ {
mutex.Lock()
counter++
mutex.Unlock()
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go increment(&wg)
go increment(&wg)
wg.Wait()
fmt.Printf("Final Counter: %d\n", counter)
}
上述代码中,两个 goroutine 并发地增加 counter
变量的值,使用互斥锁 sync.Mutex
来保护临界区,确保同一时刻只有一个 goroutine 能够访问。
在一些场景中,我们可能需要同时处理多个 channel,Golang 中提供了 select
语句来实现。
package main
import (
"fmt"
"time"
)
func producer(ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
ch <- i
}
close(ch)
}
func consumer(ch1, ch2 chan int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case num, ok := <-ch1:
if ok {
fmt.Printf("From ch1: %d\n", num)
}
case num, ok := <-ch2:
if ok {
fmt.Printf("From ch2: %d\n", num)
}
}
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
var wg sync.WaitGroup
wg.Add(2)
go producer(ch1, &wg)
go producer(ch2, &wg)
go consumer(ch1, ch2, &wg)
wg.Wait()
fmt.Println("Main goroutine")
}
上述代码中,consumer
函数通过 select
同时监听两个 channel(ch1
和 ch2
),一旦其中一个 channel 有数据,就进行处理。这种方式非常适用于处理多个 channel 的情况。
通过本文,我们深入了解了 Golang 中的并发编程,包括 goroutine、channel 以及一些常见的并发模式。并发编程使得 Golang 在处理高并发任务时表现出色,开发者可以通过合理使用并发特性来提高程序性能。
希望读者通过本文的学习,能更好地应用 Golang 中的并发编程,构建高效、稳定的软件系统。