Go语言进阶——并发编程

并发与并行

  • 并发:多个线程通过切换时间片的方式在一个cpu上进行调度运行
  • 并行:多个线程在cpu的多个核上运行,真正的同时运行

线程与协程

  • 线程:内核态,操作系统内核进行调度的基本单位,在一个线程上可以跑多个携程,栈大小在MB级别
  • 协程:用户态,由Go管理,轻量级线程,栈大小在KB级别

Go中使用go语句创建协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package concurrence

import (
"fmt"
"sync"
"time"
)

func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}

func ManyGo() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}

输入如下

可以看到输出并不是顺序的,说明确实协程是并发执行的

CSP

  • 通过通信共享内存:通过通道的方式实现进程之间信息的交换(Go语言提倡)
  • 通过共享内存实现通信:操作系统中进程通信的经典方式,通过读写信号量实现对临界区内存的正确访问

Channel

make(chan <eleType>, [size])

  • 无缓冲通道:make(chan int)
  • 有缓冲通道:make(chan int, 2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package concurrence

func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
// 协程A: 将数字放进无缓冲通道src中
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
// 协程B: 将数字从src中取出,平方后放入有缓冲通道dest
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
// 主线程: 将数字从dest中取出并打印
for i := range dest {
println(i)
}
}

Lock

使用信号量的方式对临界区的访问进行控制,使得并发的协程能够正确访问临界区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package concurrence

import "sync"

var (
x int64
lock sync.Mutex
)

func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}

func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}

测试是各创建5个协程调用上述两个方法,结果如下

可以看到加锁方法可以保证每次都正确得到结果;但是不加锁的方法每次得到的结果是不确定的

WaitGroup

前面在主线程创建了协程之后,主线程是使用time.Sleep()方法来阻塞自己的,但是这并不是一个好的方法,因为我们并不知道协程到底什么时候执行结束,我们只能传入一个大概的比较大的值进去。

sync包下有一个结构体:WaitGroup,可以通过该方法优雅地实现主进程的阻塞

该结构体内部维护了一个计数器,并且暴露了三个方法出来

  • Add:创建了多少个协程,就传入相应的delta
  • Done:当协程运行结束时,调用Done()
  • Wait:该方法用来阻塞直到所有的协程执行结束

然后就可以使用这三个方法来实现主进程的阻塞(以第一个例子为例)

1
2
3
4
5
6
7
8
9
10
11
func ManyGo() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}

Go语言进阶——并发编程
http://example.com/2023/01/16/Go/go-concurrency/
作者
zhc
发布于
2023年1月16日
许可协议