现代应用程序通常需要同时处理海量任务,如果您的应用程序没有为此做好准备,它可能会变得笨重且响应迟缓。这就是并发如此重要的原因。Go 语言(通常称为 Golang)内置了并发功能,让您可以编写更快、更灵活的软件,而无需陷入线程管理的泥潭。
在本指南中,我们将首先回顾传统程序如何按顺序执行任务,以及为什么这种方式会导致性能瓶颈。接着,我们将通过 goroutine 和通道探索 Go 的并发模型,对比并发与并行的区别,并通过实际示例(包括一个小型的真实用例)加深理解。如果您已经掌握了基本的 Go 语法,但希望深入了解 goroutine,那么这篇文章正是为您准备的。
大多数传统程序会逐条执行指令,并阻塞后续指令,直到当前指令完成为止。这种方式简单直观,但当任务耗时较长时,效率会大打折扣。
下面是个例子:
#include <stdio.h>
int main() {
printf("Step 1: Fetch user data\n");
printf("Step 2: Process user data\n");
printf("Step 3: Save user data\n");
return ;
}
预期输出:
Step 1: Fetch user data
Step 2: Process user data
Step 3: Save user data
这种顺序执行的流程虽然清晰,但如果“获取用户数据”需要几秒钟,整个程序就会停滞不前,等待该操作完成。对于需要同时处理多个用户、进程或数据源的系统来说,这无疑是一个性能瓶颈。
假设您正在构建一个需要同时处理多个请求的 Web 服务。如果服务器逐个处理这些请求,每个请求都可能需要排队等待,即使有其他任务或 CPU 内核可用。并发允许您的程序在任务之间灵活切换,充分利用等待时间(例如 I/O 等待),从而保持应用程序的高响应性。
在深入探讨 Go 的并发特性之前,我们需要澄清两个经常被混淆的概念:并发和并行。
在 Go 中,goroutine 让您可以轻松构建并发程序。如果您在多核系统上运行 Go 程序(或使用 runtime.GOMAXPROCS
),Go 会自动在可用的 CPU 核心上并行调度这些 goroutine。但即使在单核上,并发性也能确保任务不会不必要地相互阻塞。
Go 引入了 goroutine,这是一种轻量级的并发执行单元。您只需在函数调用前加上 go
关键字,即可启动一个 goroutine。
示例:基本 Goroutines
package main
import (
"fmt"
"time"
)
func task(name string) {
for i := ; i <= ; i++ {
fmt.Println(name, "running", i)
time.Sleep(time.Millisecond * )
}
}
func main() {
go task("FunTester Task 1")
go task("FunTester Task 2")
// 防止主函数提前退出
time.Sleep(time.Second * )
fmt.Println("Main function completed")
}
预期输出:
FunTester Task 1 running 1
FunTester Task 2 running 1
FunTester Task 1 running 2
FunTester Task 2 running 2
FunTester Task 1 running 3
FunTester Task 2 running 3
Main function completed
可以看到,“Task 1”和“Task 2”的输出是交错的,这正是并发性的体现。
Go 并发模型的核心思想是通过通信来共享内存,而不是直接共享内存。通道(channel) 是 goroutine 之间传递数据的安全方式。
无缓冲通道的创建方式如下:
ch := make(chan int)
当您向无缓冲通道发送数据时,发送方会阻塞,直到接收方接收到数据。
package main
import (
"fmt"
"time"
)
func sendData(ch chan<- int, data int) {
fmt.Println("Sending", data)
ch <- data // 阻塞直到接收
fmt.Println("Finished sending", data)
}
func receiveData(ch <-chan int) {
val := <-ch // 阻塞直到接收到数据
fmt.Println("Received", val)
}
func main() {
ch := make(chan int)
go sendData(ch, )
go receiveData(ch)
time.Sleep(time.Second)
fmt.Println("Done")
}
预期输出:
Sending 10
Received 10
Finished sending 10
Done
当多个 goroutine 同时访问共享数据,且至少有一个 goroutine 修改该数据时,就会发生竞争条件。为了避免这种情况,Go 提供了多种同步机制,例如 互斥锁(Mutex) 和 通道。
package main
import (
"fmt"
"sync"
)
type safeCounter struct {
mu sync.Mutex
count int
}
func (sc *safeCounter) increment() {
sc.mu.Lock()
sc.count++
sc.mu.Unlock()
}
func main() {
sc := &safeCounter{}
var wg sync.WaitGroup
wg.Add()
go func() {
defer wg.Done()
for i := ; i < ; i++ {
sc.increment()
}
}()
go func() {
defer wg.Done()
for i := ; i < ; i++ {
sc.increment()
}
}()
wg.Wait()
fmt.Println("Final count:", sc.count)
}
以下是一个简单的并发 Web 服务器示例,用于计算某个数字的阶乘。通过为每个请求启动一个 goroutine,服务器可以同时处理多个请求,而不会阻塞。
package main
import (
"fmt"
"log"
"net/http"
"strconv"
"sync"
)
func factorial(n int) int {
if n <= {
return
}
return n * factorial(n-1)
}
func main() {
var wg sync.WaitGroup
http.HandleFunc("/factorial", func(w http.ResponseWriter, r *http.Request) {
nStr := r.URL.Query().Get("n")
n, err := strconv.Atoi(nStr)
if err != nil {
http.Error(w, "Invalid number", http.StatusBadRequest)
return
}
wg.Add()
go func(num int) {
defer wg.Done()
result := factorial(num)
fmt.Fprintf(w, "Factorial(%d) = %d\n", num, result)
}(n)
})
log.Println("Server starting at :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
sync.WaitGroup
、通道和 context.Context
,避免使用 time.Sleep
来协调 goroutine。通过这些工具可以更精确地控制并发行为,减少不必要的等待。context.WithCancel
或其他机制确保不再需要的 goroutine 被正确终止。泄漏的 goroutine 会占用系统资源,影响程序性能。-race
标志检测潜在的竞争条件。竞争条件可能导致数据不一致或程序崩溃,需特别注意。