这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
一、并发编程
1.1 Goroutine/Channel
goroutine是go的协程实现,在函数前面写上关键字go,就表示go程序会起一个协程去执行这个函数。而channel是管道,用于协程之间的通信。管道有自己的类型和空间,可以理解成一个队列——协程a可以往管道里写数据,而协程b可以从管道里拿数据。管道是线程安全的,每次能有一个协程访问这个管道。下面是一个生产者消费者的例子,生产者依次生成0到9这几个数字,然后消费者消费这些数字并计算他们的平方交给用户。
首先我们用make来创建管道用来通信。make的第二个参数表示管道的大小,如果没有默认是0,也就是必须同步的读写。这里我们用了两个管道,第一个管道src是生产者用来生产数字给消费者的。消费者通过src消费了数字后,计算他们的平方,然后通过第二个管道dest传给用户。
func main() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for v := range dest {
fmt.Println(v)
}
}
1.2 sync.Mutex
sync.Mutex是go提供的互斥量,对互斥量加锁解锁可以实现对临界资源的保护,避免并发安全问题。在sync包中,sync.Mutex有两个方法,Lock和Unlock,这两个方法就是标准的互斥量用法,在进入临界区的时候加锁,退出的时候解锁。协程如果想要进入临界区,必须先获得锁。如果已经有其他的协程获取锁,并进入了临界区。那么这个协程只能等锁释放。
下面是一个并发安全问题的例子,我们用5个协程对一个临界变量做自增,每个协程都自增2000次。如果不对x加锁,显然由于并发问题会导致实际上x值小于2000 * 5。
var x int
var lock sync.Mutex
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x++
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x++
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
fmt.Println(x)
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
fmt.Println(x)
}
1.3 WaitGroup
sync.WaitGroup主要是用来做协程之间的同步的。它适用于这样的场景,一个主协程有很多子协程。主协程希望等待所有的子协程的任务执行完了以后,再执行别的任务。这时候可以用WaitGroup实现这种需求。
WaitGroup的本质就是一个计数器,它会计数当前等待的协程数量。它有3个常用的方法Add,Done和Wait。
- Add(i int) 表示计数器增加i
- Done() 表示计数器减一
- Wait() 这个方法一般是主协程调用,用来等待其他的协程执行完。
func hello(i int) {
fmt.Println("hello goroutine,", i)
}
func HelloGoroutine() {
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
hello(i)
}(i)
}
wg.Wait()
}
func main() {
HelloGoroutine()
}