Go语言进阶语法|青训营笔记(二)

70 阅读4分钟

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)
}

可以看到输出的结果是乱序的,并不是按照协程创建顺序输出。

image.png

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的平方数。

image.png 需要注意的是,当一个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。