1. 什么是Goroutines
Goroutines是Go语言中的一种轻量级线程,它的运行是由Go的调度器来完成的。当Goroutine遇到I/O等阻塞操作时,会将线程切换到其他可以运行的Goroutine上,使得程序能够更好的利用CPU资源。
下面我们来看一个简单的Goroutine实例:
package main
import (
"fmt"
"time"
)
func printHello() {
for i := 0; i < 5; i++ {
fmt.Println("Hello")
}
}
func main() {
go printHello()
for i := 0; i < 5; i++ {
fmt.Println("World")
}
time.Sleep(1 * time.Second)
}
这段代码中,我们定义了一个printHello函数,并使用keywordgo
关键字来创建一个新的Goroutine执行printHello函数。同时,我们还在主Goroutine中打印了5次"World"。要注意的是,由于Goroutine的运行是非阻塞的,因此主Goroutine在循环之后直接结束了,而新创建的Goroutine仍在运行。因此,我们使用time.Sleep()
让主Goroutine睡眠1秒来等待新的Goroutine执行完毕。
2. 使用WaitGroup等待Goroutine结束
上面的例子中,我们使用了time.Sleep()
来等待新的Goroutine执行完毕。但这种方式不仅不优雅,而且还会降低程序的速度。现在我们来介绍一个更好的等待Goroutine结束的方式-使用sync.WaitGroup
。
我们将上面的例子稍作改动,让主Goroutine在新Goroutine执行结束前等待它执行完毕,主动释放线程资源:
package main
import (
"fmt"
"sync"
)
func printHello(wg *sync.WaitGroup) {
defer wg.Done() // 通知WaitGroup已完成
for i := 0; i < 5; i++ {
fmt.Println("Hello")
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1) // 增加WaitGroup的计数器
go printHello(&wg)
wg.Wait() // 等待Goroutine执行完成
fmt.Println("World")
}
在上面的例子中,我们创建了一个sync.WaitGroup类型的变量wg
,使用wg.Add(1)
增加了一个计数器。我们将WaitGroup传递给printHello函数,在执行完后使用wg.Done()
来通知计数器已完成。最后,我们使用wg.Wait()
等待Goroutine执行完成。
3. 使用channel进行Goroutine间通信
Goroutine在Go语言的并发编程中被广泛使用,而它们之间的通信方式也尤为重要。在Go语言中,我们使用channel来进行Goroutine间的通信。channel是Go语言中的一种特殊类型,它像一个管道一样可以在Goroutine之间传递数据。简单来说,就是将要传递的数据放入管道中,其他Goroutine从管道中取出数据。
我们来看一个简单的例子,使用channel实现在两个Goroutine之间传递字符串:
package main
import (
"fmt"
)
func sender(ch chan string) {
ch <- "Hello!" // 向管道中输入数据
}
func receiver(ch chan string) {
msg := <-ch // 从管道中读取数据
fmt.Println(msg)
}
func main() {
ch := make(chan string) // 创建一个字符串类型的管道
go sender(ch)
go receiver(ch)
fmt.Scanln() // 防止程序过早退出
}
在上面的例子中,我们首先使用make(chan string)
创建了一个字符串类型的管道,以及两个Goroutine,一个用于向管道中输入数据,一个用于从管道中读取数据。我们使用keyword<-
符号进行数据的输入和输出。在最后,我们使用fmt.Scanln()
来防止程序过早退出。
3.1 带缓冲的channel
在上面的例子中,我们创建的是一个不带缓冲的channel,也就是说管道中只能存放一个数据。当管道中的数据还未被读取时,如果向管道中输入新数据,程序会一直阻塞,直到数据被读取。这在一些情况下可能会导致性能问题。在这种情况下,我们可以使用带缓冲的channel。
带缓冲的channel在创建时需要指定缓冲区的大小。例如,创建一个缓冲区大小为10的字符串类型管道:
ch := make(chan string, 10)
我们使用一个例子来展示带缓冲的channel的使用:
package main
import (
"fmt"
"time"
)
func producer(ch chan int, cnt int) {
for i := 0; i < cnt; i++ {
fmt.Printf("send %d\n", i)
ch <- i // 将数据发送到channel
}
}
func consumer(ch chan int) {
for {
value := <-ch // 从channel中取出数据
fmt.Printf("receive %d\n", value)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch, 10)
go consumer(ch)
time.Sleep(time.Second * 3)
}
在上面的例子中,我们创建了一个缓冲区大小为5的整型类型管道ch
。我们创建了两个Goroutine,一个是生产者Goroutine,一个是消费者Goroutine。生产者Goroutine向管道中发送10个数据,而消费者Goroutine从管道中取出数据进行处理。
4. 使用select语句处理channel操作
在Go语言中,我们可以使用select语句从多个channel中选择数据并执行相应的操作。它主要用于处理被阻塞的channel操作。
我们先来看一个简单的例子:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan string)
go func() {
for {
ch1 <- 1
time.Sleep(time.Second)
}
}()
go func() {
for {
ch2 <- "Hello, World!"
time.Sleep(time.Second * 2)
}
}()
for {
select {
case msg := <-ch1:
fmt.Println("received message from ch1:", msg)
case msg := <-ch2:
fmt.Println("received message from ch2:", msg)
default:
fmt.Println("no message received")
time.Sleep(time.Second)
}
}
}
在上面的例子中,我们创建了两个channel,一个是整型类型的channelch1
,一个是字符串类型的channelch2
。然后创建了两个Goroutine分别向两个channel种发送数据。接着,我们在主函数中使用一个无限循环,使用select
语句处理从两个channel中接收到的数据。在程序运行过程中,我们可以在指定的时间间隔内从不同的channel中接收到不同的数据。
总结
在Go语言中,Goroutine是实现并发编程的一种非常方便的方式,它可以让程序更好的利用多核CPU资源。在使用Goroutine时,我们需要注意合理对资源进行管理和分配,同时采取措施避免资源竞争问题。
Channel则是在Goroutine之间进行通信的重要机制,可以方便的实现数据的传输和同步。在实际使用中,我们需要灵活选择channel的类型和缓冲区大小,并合理使用select语句处理channel操作。