在 Go 语言中,goroutine 是一种轻量级的用户态线程,由 Go 运行时(runtime)管理。goroutine 的创建和销毁成本非常低,因此可以轻松地实现并发编程。
Goroutine 是 Go 语言中的并发执行单元。与操作系统线程相比,goroutine 更加轻量级,创建和销毁的开销更小。Go 运行时会在逻辑处理器(P)上调度 goroutine,使其在多个操作系统线程上高效地运行。
要在 Go 中创建一个 goroutine,只需在函数调用前加上关键字 go
。例如:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello, world!")
}
func main() {
go sayHello() // 创建一个新的 goroutine 来执行 sayHello 函数
time.Sleep(1 * time.Second) // 等待 1 秒,确保 goroutine 有机会执行
}
此时我们并不知道 goroutine什么时候执行完成,上例中我们使用睡眠,让程序停止,等待携程。
sync.WaitGroup
sync.WaitGroup
,用于等待一组 goroutine 完成。它通过计数器来跟踪正在执行的 goroutine 数量,当计数器归零时,表示所有 goroutine 已完成。
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 在函数结束时调用 Done,减少计数器
fmt.Printf("Worker %d 开始工作\n", id)
time.Sleep(time.Second) // 模拟工作耗时
fmt.Printf("Worker %d 完成工作\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 每启动一个 goroutine,计数器加一
go worker(i, &wg)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("所有工作已完成")
}
Go 语言的运行时包含一个复杂的调度器,用于在多个操作系统线程上调度 goroutine。调度器使用 M:N 模型,将 M 个 goroutine 映射到 N 个操作系统线程上。调度器会根据程序的运行情况动态地将 goroutine 分配给不同的线程。
Go 运行时负责将 goroutine 调度到一组操作系统线程上执行。默认情况下,Go 运行时会根据系统的 CPU 核心数和程序的负载动态调整使用的线程数量。这种自动管理机制使得开发者无需手动干预线程的创建和管理。
然而,在某些特定场景下,如需要限制资源使用、可能需要手动设置或控制 Go 程序使用的最大线程数。
GOMAXPROCS
是 Go 运行时的一个重要环境变量和函数,用于设置可以同时执行 Go 代码的最大 CPU 核心数。它并不直接限制操作系统线程的数量,但会影响 Go 运行时调度 goroutine 到线程上的方式。
runtime.GOMAXPROCS
设置您可以在程序启动时调用 runtime.GOMAXPROCS
来设置并行执行的 CPU 核心数:
package main
import (
"fmt"
"runtime"
)
func main() {
// 获取当前的 GOMAXPROCS 值
current := runtime.GOMAXPROCS(0)
fmt.Println("当前 GOMAXPROCS:", current)
// 设置 GOMAXPROCS 为 4
previous := runtime.GOMAXPROCS(4)
fmt.Println("之前的 GOMAXPROCS:", previous)
fmt.Println("现在的 GOMAXPROCS:", runtime.GOMAXPROCS(0))
}
输出示例:
当前 GOMAXPROCS: 8
之前的 GOMAXPROCS: 8
现在的 GOMAXPROCS: 4
说明:
runtime.GOMAXPROCS(n)
设置可以同时执行 Go 代码的最大 CPU 核心数为 n
,并返回之前的值。n
小于 1,则不会改变当前的设置,只是返回当前的值。从 Go 1.5 开始,GOMAXPROCS
的默认值被设置为运行程序的机器上的 CPU 核心数。因此,通常不需要手动设置,除非有特定需求。
在并发编程中,通信和同步是非常重要的。Go 语言提供了多种机制来实现 goroutine 之间的通信和同步。
通道是 Go 语言中的一种类型安全的通信机制,用于在 goroutine 之间传递数据。通道有两种类型:无缓冲通道和有缓冲通道。
创建一个无缓冲通道:
ch := make(chan int)
创建一个有缓冲通道:
ch := make(chan int, 10) // 缓冲区大小为 10
发送数据到通道:
ch <- 42 // 将整数 42 发送到通道 ch
从通道接收数据:
value := <-ch // 从通道 ch 接收数据,并将其赋值给变量 value
单向通道主要用于函数参数传递,以明确函数的意图,提高代码的可读性和安全性。
示例:发送数据的函数
func sendOnly(ch chan<- int, value int) {
ch <- value
}
在上述函数中,参数 ch
被声明为 chan<- int
,表示 ch
只能用于发送整数,不能用于接收数据。
示例:接收数据的函数
func receiveOnly(ch <-chan int) int {
return <-ch
}
这里,参数 ch
被声明为 <-chan int
,表示 ch
只能用于接收整数,不能用于发送数据。
使用通道实现生产者-消费者模式:
package main
import (
"fmt"
"sync"
)
func producer(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("生产者发送:", i)
}
close(ch) // 发送完毕后关闭通道
}
func consumer(ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for value := range ch { // 使用range接收,直到通道关闭
fmt.Println("消费者接收:", value)
}
fmt.Println("消费者检测到通道已关闭")
}
func main() {
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(2)
go producer(ch, &wg)
go consumer(ch, &wg)
wg.Wait()
fmt.Println("所有操作完成")
}
输出示例:
生产者发送: 0
消费者接收: 0
生产者发送: 1
消费者接收: 1
生产者发送: 2
消费者接收: 2
生产者发送: 3
消费者接收: 3
生产者发送: 4
消费者接收: 4
消费者检测到通道已关闭
所有操作完成
在这个示例中:
producer
goroutine 向通道发送了 5 个整数,然后关闭了通道。consumer
goroutine 使用 for range
循环接收通道中的数据,直到通道被关闭。关闭通道的责任:只有发送方应该关闭通道。如果接收方尝试关闭通道,可能会导致运行时恐慌,尤其是在多个发送方的情况下。
避免重复关闭:确保通道只被关闭一次。可以通过使用 sync.Once
或其他同步机制来保证这一点。
接收方的处理:接收方应该能够正确处理通道关闭的情况。例如,使用 for range
循环可以自动检测通道是否已关闭。
缓冲通道:对于有缓冲的通道,关闭通道的行为与无缓冲通道类似,但接收方可以在缓冲区中的数据被消费完之前继续接收数据。
select
语句类似于 switch
,但是用于处理多个通道操作。它允许程序等待多个通道操作中的任意一个完成,并执行对应的代码块。select
的基本语法:
select {
case <-ch1:
// 处理来自 ch1 的数据
case ch2 <- value:
// 向 ch2 发送数据
default:
// 如果没有任何 case 可运行,则执行 default
}
select
语句通常与多个通道操作一起使用,以处理并发任务。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// 启动两个发送数据的 goroutine
go func() {
time.Sleep(2 * time.Second)
ch1 <- "来自 ch1 的消息"
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- "来自 ch2 的消息"
}()
// 使用 select 等待任意一个通道有数据
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
输出:
来自 ch2 的消息
在这个例子中,ch2
先有数据可接收,因此先打印来自 ch2
的消息。
select
可以包含一个 default
分支,当没有任何 case
可运行时,执行 default
分支。这使得 select
成为一个非阻塞操作。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
// 尝试接收数据,如果 ch 没有数据,则执行 default
select {
case msg := <-ch:
fmt.Println("接收到:", msg)
default:
fmt.Println("没有接收到数据")
}
}
输出:
没有接收到数据
结合 time.After
,可以实现超时控制,防止 select
永远阻塞。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
// 启动一个发送数据的 goroutine,但会延迟3秒
go func() {
time.Sleep(3 * time.Second)
ch <- "消息"
}()
// 设置超时时间为2秒
select {
case msg := <-ch:
fmt.Println("接收到:", msg)
case <-time.After(2 * time.Second):
fmt.Println("接收超时")
}
}
输出:
接收超时
通过在 select
中添加 default
分支,可以实现非阻塞的发送和接收。
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
// 非阻塞发送
select {
case ch <- 1:
fmt.Println("发送成功")
default:
fmt.Println("发送失败,通道已满或无接收方")
}
// 非阻塞接收
select {
case msg := <-ch:
fmt.Println("接收到:", msg)
default:
fmt.Println("接收失败,通道为空")
}
}
输出:
发送成功
接收到: 1
如果将通道缓冲区设置为0,并尝试非阻塞发送,可能会失败:
ch := make(chan int) // 无缓冲通道
// 非阻塞发送,没有接收方准备好,发送失败
select {
case ch <- 1:
fmt.Println("发送成功")
default:
fmt.Println("发送失败,通道已满或无接收方")
}
输出:
发送失败,通道已满或无接收方
下面是一个综合示例,展示如何使用 select
处理多个管道的发送和接收,以及超时控制。
package main
import (
"fmt"
"time"
)
func producer(ch chan<- string, name string, delay time.Duration) {
time.Sleep(delay)
ch <- fmt.Sprintf("来自 %s 的消息", name)
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go producer(ch1, "生产者1", 2*time.Second)
go producer(ch2, "生产者2", 1*time.Second)
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("接收超时")
}
}
}
输出:
来自 生产者2 的消息
来自 生产者1 的消息
如果将 for
循环的条件改为 i < 1
,并添加超时控制,可以观察到超时行为。
select
语句阻塞主线程以防止程序退出在某些情况下,尤其是当所有的工作都在其他 goroutine 中进行时,主 goroutine 可能需要等待其他 goroutine 完成工作。这时可以使用一个空的 select
语句来阻塞主线程。
示例:等待其他 goroutine
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Println("工作开始")
time.Sleep(2 * time.Second)
fmt.Println("工作完成")
done <- true
}
func main() {
done := make(chan bool)
go worker(done)
// 等待工作完成,可以通过接收 done 信号来实现
// 但这里为了简单起见,使用空的 select 阻塞主线程(不推荐实际使用)
// select {}
// 更好的做法是使用 <-done 来等待
<-done
fmt.Println("主程序退出")
}
注意:
select {}
,主线程将被永久阻塞,程序不会退出。但更好的做法是使用 <-done
来等待工作完成。Go 语言还提供了其他同步原语,如互斥锁(sync.Mutex)和读写锁(sync.RWMutex),用于保护共享资源。
使用互斥锁保护共享资源:
package main
import (
"fmt"
"sync"
)
var counter int
var mu sync.Mutex
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
sync.Mutex
提供了两个主要的方法:
Lock()
: 获取锁。如果锁已被其他goroutine持有,则当前goroutine会被阻塞,直到锁被释放。Unlock()
: 释放锁,允许其他goroutine获取该锁。读写锁是一种比互斥锁更细粒度的锁机制,适用于读多写少的场景。它允许多个goroutine同时读取共享资源,但在写操作时需要独占访问。这可以提高并发性能,减少锁竞争。
sync.RWMutex
提供了以下方法:
RLock()
: 获取读锁。如果当前没有写锁被持有,多个读锁可以被同时获取。RUnlock()
: 释放读锁。Lock()
: 获取写锁。写锁是排他的,当写锁被持有时,其他所有的读锁和写锁都会被阻塞。Unlock()
: 释放写锁。:在需要支持大量并发读取操作的场景下,读写锁比互斥锁更高效。
package main
import (
"fmt"
"sync"
"time"
)
var (
data = make(map[string]string)
rwMutex sync.RWMutex
)
func readData(key string, wg *sync.WaitGroup) {
defer wg.Done()
rwMutex.RLock()
value, exists := data[key]
fmt.Printf("Read %s: %s (exists: %v)\n", key, value, exists)
rwMutex.RUnlock()
}
func writeData(key, value string, wg *sync.WaitGroup) {
defer wg.Done()
rwMutex.Lock()
data[key] = value
fmt.Printf("Write %s: %s\n", key, value)
rwMutex.Unlock()
}
func main() {
// 初始化一些数据
writeData("name", "Alice", &sync.WaitGroup{})
writeData("age", "30", &sync.WaitGroup{})
var wg sync.WaitGroup
// 启动多个读goroutine
for i := 0; i < 5; i++ {
wg.Add(1)
go readData("name", &wg)
}
// 启动一个写goroutine
wg.Add(1)
go writeData("name", "Bob", &wg)
// 再启动多个读goroutine
for i := 0; i < 5; i++ {
wg.Add(1)
go readData("name", &wg)
}
wg.Wait()
}
在上述示例中,多个goroutine可以同时读取data
中的数据,而写操作则需要独占锁。这确保了在读取过程中数据的一致性,同时允许多个读操作并发执行,提高了性能。
sync.Once
sync.Once
的内部实现保证了 Do
方法只会执行一次,即使在多个 goroutine 同时调用的情况下。
这种实现保证了高效且线程安全的单次执行。
**Do(f func())
**: 执行传入的函数 f
,确保该函数在整个程序生命周期内只被调用一次。如果有多个 goroutine 同时调用 Do
,只有一个 goroutine 会执行 f
,其他 goroutine 会被阻塞,直到 f
执行完毕。
sync.Once
内部维护了一个状态标志和一个同步机制(如互斥锁和条件变量),以确保:
Do
时,执行传入的函数,并将状态标志设置为已执行。Do
时,直接跳过函数执行,不会再次执行传入的函数。package main
import (
"fmt"
"sync"
)
var (
once sync.Once
instance *Singleton
)
type Singleton struct {
Name string
}
func GetInstance() *Singleton {
once.Do(func() {
fmt.Println("Initializing Singleton...")
instance = &Singleton{Name: "Unique Instance"}
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
singleton := GetInstance()
fmt.Printf("Goroutine %d: %+v", id, singleton)
}(i)
}
wg.Wait()
}
输出示例:
Initializing Singleton...
Goroutine 0: &{Name:Unique Instance}
Goroutine 1: &{Name:Unique Instance}
Goroutine 2: &{Name:Unique Instance}
...
在上述示例中,无论有多少个 goroutine 调用 GetInstance
,Singleton
的初始化代码只会执行一次。
在并发编程中,错误处理和恢复尤为重要。Go 语言提供了 defer
和 recover
机制来处理 panic 和恢复程序的执行。
package main
import (
"fmt"
)
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
go func() {
panic("Something went wrong!")
}()
select {} // 阻塞主 goroutine,防止程序退出
}
Go 语言的 goroutine 提供了一种简单而强大的并发编程模型。通过使用通道和同步原语,可以轻松地在 goroutine 之间进行通信和同步。同时,Go 语言的错误处理和恢复机制也有助于编写健壮的并发程序。