Go语言进阶语法|青训营笔记(二)
这是我参与「第五届青训营 」笔记创作活动的第2天,本文是介绍了Go语言的一些进阶语法,包括并发、通道、WaitGroup、共享内存并发、Lock等
1.并发
Go语言支持并发程序,并发是指能同时进行多个任务。随着硬件的发展,并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。
Go语言中的并发程序可以用goroutine来实现,goroutine是一种轻量级线程,也可以成为协程。协程的调度是由 Go 运行时调度和管理的。创建goroutine可以使用go关键字,可以创建一个协程来执行某个函数,其格式如下:
go 函数名( 参数列表 )
下面是一个并发的实例代码,循环创建了五个协程,每个协程都执行输出hello world以及对应的序号。
package main
import (
"fmt"
"time"
)
func hello(i int) {
fmt.Println("hello world" + fmt.Sprint(i))
}
func main() {
for i := 0; i < 5; i++ {
go hello(i) //开启一个协程
}
time.Sleep(time.Second)
}
可以看到输出的结果是乱序的,并不是按照协程创建顺序输出。
2.通道(channel)
通道是用于传递数据的一种数据结构,在go语言中可以通过channel实现对两个goroutine同步、通信等。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。通道使用make关键字创建,其格式如下:
ch := make(chan int,[num])
其中,num可以用来指定通道缓冲区大小,若省略则表示创建无缓冲通道。
下面为使用通道实现goroutine通信的一个实例
package main
func main() {
count := make(chan int) //创建无缓冲通道
squre := make(chan int)
go func() {
for i := 1; i < 10; i++ {
count <- i
}
close(count) //结束后需要关闭通道,否则会发生阻塞
}()
go func() {
for i := range count {
squre <- i * i
}
close(squre) //结束后需要关闭通道,否则会发生阻塞
}()
for i := range squre {
println(i)
}
}
实例中创建了一个用于计数的goroutine和一个用于计算平方的goroutine,主goroutine打印计算结果。同时创建了两个channel将多个goroutine链接在一起。运行结果按顺序打印除了1~9的平方数。
需要注意的是,当一个
goroutine执行完毕后需要关闭相关的channel,否则可能会发生通道阻塞问题。
3.WaitGroup
当一个程序的主协程结束后,它的所有子协程也会自动结束,这时子协程的任务可能还没有完成,这显然是有问题的。为了知道所有的子协程什么时候结束,Go语言中可以使用WaitGroup来实现协程之间的同步。WaitGroup可以看作一个计数器,当有新的协程被创建时,计数器就加1;而当一个协程结束时,计数器就减1;当计数器为0则表示当前没有子协程运行,可以退出主协程。
WaitGroup主要有三个方法:Add(),Done(),Wait()
Add():计数器加1;Done():计数器减1;Wait():当计数器不为零时,阻塞主协程直到计数器为零。
下面为第一节并发实例的改善版,使用WaitGroup来等待子协程完成。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello(i int) {
fmt.Println("hello world" + fmt.Sprint(i))
wg.Done()
}
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go hello(i) //开启一个协程
}
wg.Wait()
}
4.共享内存的并发
Go语言除了通过使用channel来进行并发程序之间的通信,即通过通信实现共享内存,还支持通过共享内存实现通信,即允许不同协程访问同一内存从而实现协程之间的通信。而使用共享内存完成通信就存在多个协程同时访问同一内存的可能。如下为一共享内存并发的实例:
package main
import (
"fmt"
"sync"
)
var (
x int64
lock sync.Mutex
wd sync.WaitGroup
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock() //加锁
x += 1
lock.Unlock() //解锁
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func main() {
//未加锁
x = 0
for i := 0; i < 5; i++ {
wd.Add(1)
go func() {
addWithoutLock()
wd.Done()
}()
}
wd.Wait()
fmt.Printf("withoutlock:%d\n", x)
//加锁
x = 0
for i := 0; i < 5; i++ {
wd.Add(1)
go func() {
addWithLock()
wd.Done()
}()
}
wd.Wait()
fmt.Printf("withlock:%d", x)
}
输出:
withoutlock:6697
withlock:10000
本实例的创建了五个协程,每个协程都完成对一共享内存加1两千次的任务,则最终结果预期应该为10000;但可以看到没有加锁的输出是6697,与预期输出不同,这是因为在并发中多个协程可能会同时访问该共享内存,出现并发安全问题。
而解决方法就是在协程操作共享内存时先进行加锁操作,对共享内存权限进行控制,使得其他协程不能访问该内存,避免了并发安全问题。这里调用Mutex的Lock方法来获取一个互斥锁,在对内存进行操作前上锁,操作完毕后再解锁,最终可以看到加锁的输出正好是预期值10000。