多个 Goroutines
Goroutines 是 Go 语言中轻量级的并发机制,它允许我们同时执行多个任务,而且相比于传统的并发技术,它的实现和开销都更为高效。
在这篇文章中,我们将会讨论如何启动和管理多个 Goroutines,以及如何确保它们之间的安全协作。
启动 Goroutines
在 Go 语言中启动一个 Goroutine 非常简单,只需要在函数调用前面添加关键字 go
即可,比如:
func main() {
go sayHello()
fmt.Println("Hello from main")
}
func sayHello() {
fmt.Println("Hello from Goroutine")
}
在上面的例子中,我们启动了一个 Goroutine 来执行 sayHello()
函数,并在同时打印了 "Hello from main"
。由于 sayHello()
执行在一个单独的 Goroutine 内,它的执行和 main 函数的打印操作是异步的,因此我们将会看到两行不同的输出。
需要注意的是,Goroutine 的执行并不受主函数的约束,如果主函数运行结束后 Goroutine 仍未完成,那么它会随着主函数的关闭而被终止。
传递参数
我们可以通过在 Goroutine 的函数调用后面添加参数的方式,把一些值传递给 Goroutine 内部的函数,比如:
func main() {
go sayHello("Bob")
fmt.Println("Hello from main")
}
func sayHello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
在上面的例子中,我们在 Goroutine 的函数调用后面传递了一个参数 "Bob"
,并在 Goroutine 内部的打印语句中使用了这个参数。由于 Goroutine 执行是异步的,所以我们不能保证 "Hello, Bob!"
的打印顺序和 "Hello from main"
的顺序一定一致,但是它们都将在 main 函数返回前完成执行。
同步操作
由于 Goroutines 的执行是异步的,当我们处于多线程状态时,我们需要考虑如何保证它们之间的协作并发的安全。这是非常关键的一个问题。
在 Go 语言中,我们可以使用 mutex、channel 及 WaitGroup 来达到这一目的。
Mutex
Mutex(互斥锁)是同步 Goroutine 的一种简单方式,它能够锁住任意资源,然后使其他 Goroutine 等待这个锁被释放之后才能够访问这个资源。
在下面的示例中,我们创建了一个互斥锁,并使用 Lock 和 Unlock 方法来访问受锁保护的变量。使用互斥锁保护的变量可以确保单一 Goroutine 在同一时刻访问它,避免了数据竞争。
var counter int = 0
var mutex sync.Mutex
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go incrementCounter(&wg)
}
wg.Wait()
fmt.Println("Counter:", counter)
}
func incrementCounter(wg *sync.WaitGroup) {
mutex.Lock()
counter++
mutex.Unlock()
wg.Done()
}
在上面的代码中,我们创建了一个互斥锁,并使用 Lock 和 Unlock 方法保护了 counter
这个变量。在 incrementCounter 函数内部,我们使用了 Lock 方法来确保单一 Goroutine 在访问 counter
变量时候被锁住,接着我们对 counter
变量进行了递增操作,最后使用了 Unlock 方法来释放锁。
Channel
Channel 是 Goroutine 之间进行通信的主要方式。它是一种类型安全的、阻塞的消息传递机制,可以用来在不同的 Goroutine 之间交换信息。
在下面的示例中,我们创建了一个 channel,然后在两个 Goroutine 之间传递消息,其中第一个 Goroutine 向 channel 内部写入了一个消息,第二个 Goroutine 从该 channel 中读取这个消息并进行了打印。
func main() {
message := make(chan string)
go func() {
message <- "Hello from Goroutine"
}()
fmt.Println(<-message)
}
在上面的代码中,我们创建了一个 channel 并将其赋值给 message 变量,接着我们用一个内联函数开启了一个 Goroutine,向这个 channel 内部写入了一条信息 "Hello from Goroutine"
,最后在 main 函数内部使用 <-
运算符从 message channel 中读取这个消息并打印。
需要注意的是,当我们使用 <-
运算符从 channel 中读取消息时,这个运算符将阻塞当前 Goroutine 直到 channel 中有数据可以读取为止,因此我们可以将 channel 看做是一种阻塞机制。
WaitGroup
WaitGroup 可以让我们等待一系列 Goroutine 的执行完成,而不用事先知道这些 Goroutine 执行的次数。
在下面的代码中,我们首先使用 for 循环生成了 5 个 Goroutine 并将其启动。然后我们再开启一个额外的 Goroutine 来等待这 5 个 Goroutine 的执行完成,并在执行完成后进行打印。
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go process(&wg)
}
wg.Wait()
fmt.Println("All Goroutine done!")
}
func process(wg *sync.WaitGroup) {
time.Sleep(time.Second)
fmt.Println("Goroutine done!")
wg.Done()
}
在上面的代码中,我们首先定义了一个 WaitGroup 变量 wg
,然后使用 for 循环生成了 5 个 Goroutine 并开启对应的 process
函数。在 process
函数内部,我们使用 time 包中定义的 time.Sleep(time.Second)
函数模拟了一段耗时的操作,随后我们使用 wg.Done()
方法标记了当前 Goroutine 的执行结束,并等待其他 Goroutine 执行完成。在 main 函数内部,我们使用 wg.Wait()
方法来等待所有 Goroutine 的执行完成,最后在控制台内打印出所有 Goroutine 执行结束的标志语。
结论
在这篇文章中,我们详细介绍了如何启动和管理多个 Goroutine,包括使用 Mutex、Channel 和 WaitGroup 等多种用于同步 Goroutine 的技术。在 Go 语言的多线程编程中,了解如何正确地启动、停止和同步 Goroutine 的执行是至关重要的,也是实现高效、安全并发程序必备的基本技能之一。