Golang并发编程:如何正确使用Goroutines

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操作。

后端开发标签