go并发实践
并发概述
1.进程与线程
- 进程就是程序在操作系统的一次执行过程, 进程是操作系统分配资源的基本单位
- 线程可以理解为一个进程的执行实体,运行在CPU上的执行单元,线程是操作系统CPU调度的基本单位
2.协程
协程可以理解为用户态县城,是更微量级的线程。协程的调度在用户态进行,不需要切换到内核态,
- 协程有独立的栈空间, 但是共享堆空间
- 一个进程可以跑多个线程,一个线程上可以跑多个协程
3.并行与并发
两个任务同时运行就是并行。
每个任务执行一小段,交叉执行,就是并发。
Goroutine
goroutine 就是go对协程的支持,一般的线程栈大小为2MB,通过创建线程池来管理一定数量的线程。
一个goroutine栈在其生命周期开始时占用空间很小,当需要某个任务并发执行时,只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数。
Goroutine使用
1
func()
2
go func() // 并发执行这个函数
主协程
go程序的入口是main函数,程序开始时,go程序回味main函数创建一个默认的goroutine,我们称之为主协程。
1
func myGoroutine() {
2
fmt.Println("myGoroutine!!!")
3
}
4
func main() {
5
go myGoroutine()
6
fmt.Println("end!!!")
7
}
8
9
//输出结果
10
end!!!
当主协程结束的时候,其他协程不管是否运行完,都会直接结束。
1
func myGoroutine() {
2
fmt.Println("myGoroutine!!!")
3
}
4
func main() {
5
go myGoroutine()
6
fmt.Println("end!!!")
7
time.Sleep(2 * time.Second)
8
}
9
//输出
10
end!!!
11
myGoroutine!!!
12
多协程调用
1
func myGoroutines(name string) {
2
for i := 0; i < 5; i++ {
3
fmt.Printf("myGoroutine %s\n", name)
4
time.Sleep(10 * time.Millisecond)
5
}
6
}
7
func main() {
8
go myGoroutines("goroutine1")
9
go myGoroutines("goroutine2")
10
time.Sleep(2 * time.Second)
11
}
12
//输出
13
myGoroutine goroutine1
14
myGoroutine goroutine2
15
myGoroutine goroutine2
16
myGoroutine goroutine1
17
myGoroutine goroutine2
18
myGoroutine goroutine1
19
myGoroutine goroutine1
20
myGoroutine goroutine2
21
myGoroutine goroutine1
22
myGoroutine goroutine2
recover捕获范围
1
func main() {
2
defer func() {
3
if e := recover(); e != nil {
4
fmt.Printf("main recover:%v\n", e)
5
}
6
}()
7
go func() {
8
defer func() {
9
if e := recover(); e != nil {
10
fmt.Printf("sub recover:%v\n", e)
11
}
12
}()
13
panic("sub func panic!!!!") // 发生panic后,不会打印
14
fmt.Println("1111")
15
}()
16
panic("main func panic!!!")
17
fmt.Println("222") // 发生panic后,不会打印
18
time.Sleep(2 * time.Second)
19
}
20
//结果
21
main recover:main func panic!!!
22
sub recover:sub func panic!!!!
主函数goroutine 中recover只能捕获主goroutine 中发生panic,子goroutine 只能捕获子goroutine发生的panic。
recover 捕获次数
一个recover 只能捕获一次panic,且一一对应。
1
2
func main() {
3
defer func() {
4
if e := recover(); e != nil {
5
fmt.Printf("recover:%v\n", e)
6
}
7
}()
8
panic("panic1")
9
panic("panic2")
10
fmt.Println("1111") //发生panic,不会打印
11
}
12
//结果
13
recover:panic1
绑定recover创建goroutine
将写成的逻辑封装成函数,绑定recover,以此创建goroutine
1
func withGoroutine(opts ...func() error) (err error) {
2
var wg sync.WaitGroup
3
for _, opt := range opts {
4
wg.Add(1)
5
//开启goroutine,做并行处理
6
go func(handler func() error) {
7
8
defer func() {
9
//协程内部捕获panic
10
if e := recover(); e != nil {
11
fmt.Printf("recover:%v\n", e)
12
}
13
14
wg.Done()
15
}()
16
17
e := handler() // 真正的逻辑调用
18
// 取第一个报错的handler 调用的错误返回
19
if err == nil && e != nil {
20
err = e
21
}
22
}(opt) //将goroutine的函数逻辑通过封装成的函数变量传入
23
}
24
wg.Wait() //等待所有的协程执行完
25
return
26
}
27
28
func main() {
29
handler1 := func() error {
30
panic("handler1 fail")
31
return nil
32
}
33
handler2 := func() error {
34
panic("handler2 fail")
35
return nil
36
}
37
38
err := withGoroutine(handler1, handler2) // 并发执行handler1 和handler2 两个任务,返回第一个报错的任务错误
39
if err != nil {
40
fmt.Printf("err is %v\n", err)
41
}
42
}
43
//输出
44
recover:handler2 fail
45
recover:handler1 fail
46
Channel
不同的goroutine之间能够通信,需要用到channel.
channel是一个可以收发数据的管道。
channel初始化
1
var channel_name chan channel_type
2
var channel_name [size]chan channel_type // 声明一个channel数组,其容量大小为size
3
4
//声明管道,需要进行初始化为其分配空间,
5
channel_name = make(chan channel_type)
6
channel_name = make(chan channel_type, size) //带有缓存的管道,size为缓存大小
7
8
//或者一步完成
9
channel_name := make(chan channel_type)
10
channel_name := make(chan channel_type, size) //带有缓存的管道,size为缓存大小
Channel 操作
1
ch := make(chan int) // 创建一个管道ch
2
ch <- v // 向管道中发送数据v
3
v := <-ch // 从管道中读取数据存储到变量v
4
close(ch) //关闭管道ch
管道用完后,需要close(ch) 对其进行关闭。
1
func main() {
2
3
ch := make(chan int, 5)
4
ch <- 1
5
ch <- 2
6
ch <- 3
7
ch <- 4
8
close(ch)
9
defer func() {
10
for i := 0; i < 5; i++ {
11
v := <-ch
12
fmt.Printf("v=%v\n", v)
13
}
14
}()
15
time.Sleep(2 * time.Second)
16
}
17
//结果
18
v=1
19
v=2
20
v=3
21
v=4
22
v=0
23
创建一个缓存为5的int类型的管道,向管道中写入一个1,2,3,4, 将管道关闭,开启一个goroutine从管道读取数据,读取5次,可以看到即便管道管理,仍然可以读取数据,读完数据后, 一直读取零值。
如何判定管道中的数据已经读完了?
判定读取
1
func main() {
2
3
ch := make(chan int, 5)
4
ch <- 1
5
ch <- 2
6
ch <- 3
7
ch <- 4
8
close(ch)
9
defer func() {
10
for i := 0; i < 5; i++ {
11
v, ok := <-ch
12
if ok {
13
fmt.Printf("v=%v\n", v)
14
} else {
15
fmt.Printf("channel数据已读完\n")
16
}
17
}
18
}()
19
time.Sleep(2 * time.Second)
20
}
21
// 结果
22
23
v=1
24
v=2
25
v=3
26
v=4
27
channel数据已读完
for range读取
1
func main() {
2
3
ch := make(chan int, 5)
4
ch <- 1
5
ch <- 2
6
ch <- 3
7
ch <- 4
8
close(ch)
9
defer func() {
10
for v := range ch {
11
fmt.Printf("v=%v\n", v)
12
}
13
}()
14
time.Sleep(2 * time.Second)
15
}
16
//输出
17
v=1
18
v=2
19
v=3
20
v=4
21
单向channel 和双向channel
1
//单向读channel
2
var ch = make(chan int)
3
type RChannnel = <-chan int
4
var rec RChannel = ch
5
6
7
//单向写channel
8
var ch = make(chan int)
9
type SChannnel = chan<- int // 定义类型
10
var send SChannel = ch
1
type RChannel = <-chan int
2
type SChannel = chan<- int
3
4
func main() {
5
6
var ch = make(chan int)
7
go func() {
8
var send SChannel = ch
9
fmt.Println("send:100")
10
send <- 100
11
}()
12
13
go func() {
14
var recv RChannel = ch
15
num := <-recv
16
fmt.Printf("receive:%d\n", num)
17
}()
18
time.Sleep(2 * time.Second)
19
}
20
//输出
21
send:100
22
receive:100
创建一个双向管道, 分别定义两个单向channel类型SChannel 和RChannel, 一个只用于发送,一个只用于读取。
不以共享内存来通信,而以通信来共享内存。
协程之间利用Channel 来传递数据。
1
func sum(s []int, c chan int) {
2
sum := 0
3
for _, v := range s {
4
sum += v
5
}
6
c <- sum // send sum to c
7
8
}
9
10
func main() {
11
s := []int{7, 2, 8, -9, 4, 0}
12
c := make(chan int)
13
go func() {
14
sum(s[:len(s)/2], c)
15
}()
16
go sum(s[len(s)/2:], c)
17
x, y := <-c, <-c
18
fmt.Println(x, y, x+y)
19
}
20
//输出
21
22
-5 17 12
channel 分为两类: 有缓冲channel 和 无缓冲channel 。
为了协程安全, 不管有无缓冲channel , 内部都会有一把锁 来控制并发访问。
channel 底层有一个队列,来存储数据。
无缓冲 channel 可以理解为 同步模式, 写入一个消息, 如果没有消费者消费,写入就回阻塞
有缓冲channel 可以理解为 异步模式, 写入消息,如果没有被消费,只要队列没满,可以继续写入。
如果缓冲channel队列满了,发送就回阻塞。

1
func add(ch chan bool, num *int) {
2
ch <- true
3
*num = *num + 1
4
<-ch
5
}
6
func main() {
7
ch := make(chan bool, 1)
8
9
var num int
10
for i := 0; i < 100; i++ {
11
go add(ch, &num)
12
}
13
time.Sleep(2 * time.Second)
14
fmt.Println("num的值: ", num)
15
}
16
//结果
17
num的值: 100
ch<-true 和 <-chan 相当于一个锁,将 *num = *num + 1 这个操作锁住了。
channel的总结
- 关闭一个未初始化的channel 会产生 panic
- channel 只能关闭一次,对同一个channel 重复关闭会产生panic
- 向一个关闭的channel 发送消息会产生panic
- 从一个已关闭的channel读取消息不回发生panic, 会一直读取道零值
- channel可以读端和写端都可以有多个goroutine 操作, 在一段关闭channel的时候, 该channel 读端的所有goroutine 都会收到channel已关闭的消息
- channel是并发安全的, 多个channel 同时读取channel的数据,不会产生并发的安全问题
go Sync
go语言中 使用通信共享内存, goroutine 之间通过 channel 来协作。go语言也支持提供对共享内存并发安全机制。
sync.WatiGroup
time.Sleep() 方法让主goroutine等待一段时间 以便 子goroutine 能够执行完打印结果。 这不是一个好的办法
1.channel
1
func main() {
2
ch := make(chan struct{}, 10)
3
for i := 0; i < 10; i++ {
4
go func(i int) {
5
fmt.Printf("num:%d\n", i)
6
ch <- struct{}{}
7
}(i)
8
}
9
10
for i := 0; i < 10; i++ {
11
<-ch
12
}
13
fmt.Println("end")
14
}
15
//结果
16
num:0
17
num:8
18
num:9
19
num:2
20
num:1
21
num:4
22
num:6
23
num:7
24
num:3
25
num:5
26
end
27
2.sync.WaitGroup
可以使用sync包下的WaitGroup来实现, 通过使用sync.WaitGroup 来实现并发任务的同步以及协程任务的等待。
sync.WaitGroup 是一个对象,里面维护着一个计数器, 通过三个方法来配合使用
- (wg *WaitGroup) Add(delta int) 计数器加 delta
- (wg *WaitGroup) Done() 计数器减1
- (wg *WaitGroup) Wait() 会阻塞代码的运行,直至计数器减为0
1
var wg sync.WaitGroup
2
3
func myGoroutine() {
4
defer wg.Done()
5
fmt.Println("myGoroutine!!")
6
}
7
8
func main() {
9
wg.Add(10)
10
for i := 0; i < 10; i++ {
11
go myGoroutine()
12
}
13
wg.Wait()
14
fmt.Println("end!!!!")
15
}
16
//结果
Sync.WaitGroup 对象的计数器不能为负数, 否则会panic, 在使用过程中, 需要保证add() 的参数值,以及执行完Done() 之后的计数器大于等于零。
sync.Once
程序中很多的逻辑只需要执行一次,例如项目工程里配置文件的加载,只需要加载一次。
sync.Once 可以在代码的任意位置初始化和调用, 并且线程安全。 sync.Once 最大的作用就是延迟初始化。对于一个使用sync.Once变量,并不会在程序启动的时候初始化,而是在第一次使用它的时候才会初始化,只初始化一次之后就留在内存里。
1
//声明配置结构体Config
2
type Config struct {}
3
4
var instance *Config
5
var once sync.Once // 声明一个sync.Once 变量
6
//获取配置结构体
7
func InitConfig() *Config {
8
once.Do(func() {
9
instance = &Config{}
10
})
11
return instance
12
}
只有在第一次调用 InitConfig() 获取Config 指针的时候才会执行 once.Do() ,执行完之后 instance 就驻留在内存中, 后面再次执行InitConfig() 的时候, 就直接返回内存中的instance.
与init()的区别
init方法是在其所在的package首次加载时执行的, sync.Once 可以在代码的任意位置初始化和调用,在第一次用的时候才会初始化。
sync.Lock
go语言中,有两种方式来控制并发安全,锁和原子操作
1
var num int
2
3
func add1() {
4
num += 1
5
}
6
7
func main() {
8
for i := 0; i < 10000; i++ {
9
go add1()
10
}
11
time.Sleep(2 * time.Second)
12
fmt.Println(num)
13
}
14
//输出
15
9422
同一时间有多个goroutine都在对num做+1操作,可能两次运行的num的初始相同,所以相当于num+1被后一个给覆盖了。
锁
互斥锁 Mutex
互斥锁 是一种常用的并发控制安全的方法,在同一时间只允许一个goroutine对共享资源进行访问。
1
//互斥锁的声明
2
var lock sync.Mutex
3
//两个方法
4
func (m *Mutex) Lock() //加锁
5
func (m *Mutex) Unlock() //解锁
一个互斥锁只能同时被一个goroutine 锁定,其他goroutine将阻塞直到互斥锁被解锁才能加锁成功。
未锁定的互斥锁 解锁将产生运行错误。
1
var num int
2
3
func add1(wg *sync.WaitGroup, mu *sync.Mutex) {
4
mu.Lock() //加锁
5
defer func() {
6
wg.Done() // 计数器-1
7
mu.Unlock() //解锁
8
}()
9
num += 1
10
11
}
12
13
func main() {
14
var wg sync.WaitGroup
15
var mu sync.Mutex
16
17
wg.Add(10) //开启10个goroutine 计数器加10
18
19
for i := 0; i < 10; i++ {
20
go add1(&wg, &mu)
21
}
22
wg.Wait() //等待所有协程执行完
23
24
fmt.Println(num)
25
}
26
27
// 输出
28
10
读写锁RWMutex
读写锁就是将读操作和写操作 分开, 分别对读和写加锁, 一般用在大量读操作, 少量写操作的时候。
1
func (rw *RWMutex) Lock() //对写操作加锁
2
func (rw *RWMutex) Unlock() //对写操作解锁
3
4
func (rw *RWMutex) RLock() //对读操作加锁
5
func (rw *RWMutex) RUnlock() //对读操作解锁
6
需要遵循几个守则:
- 同时只能有一个goroutine能够获得写锁定
- 同时可以有 任意多个goroutine 获得读锁定
- 同时只能存在写锁定或读锁定(读和写互斥)
1
ar cnt int
2
3
func main() {
4
var mr sync.RWMutex
5
for i := 1; i <= 3; i++ {
6
go write(&mr, i)
7
}
8
9
for i := 1; i <= 3; i++ {
10
go read(&mr, i)
11
}
12
time.Sleep(2 * time.Second)
13
fmt.Println("final count:", cnt)
14
}
15
16
func read(mr *sync.RWMutex, i int) {
17
fmt.Printf("goroutine%d reader start\n", i)
18
mr.RLock()
19
fmt.Printf("goroutine%d reading count:%d\n", i, cnt)
20
time.Sleep(time.Millisecond)
21
mr.RUnlock()
22
23
fmt.Printf("goroutine%d reader over\n", i)
24
}
25
26
func write(mr *sync.RWMutex, i int) {
27
fmt.Printf("goroutine%d writer start\n", i)
28
mr.Lock()
29
cnt++
30
fmt.Printf("goroutine%d writing count:%d\n", i, cnt)
31
time.Sleep(time.Millisecond)
32
mr.Unlock()
33
fmt.Printf("goroutine%d writer over\n", i)
34
}
35
//输出
36
37
goroutine3 reader start
38
goroutine3 reading count:0
39
goroutine3 writer start
40
goroutine1 reader start
41
goroutine1 writer start
42
goroutine2 reader start
43
goroutine2 writer start
44
goroutine3 reader over
45
goroutine3 writing count:1
46
goroutine3 writer over
47
goroutine1 reading count:1
48
goroutine2 reading count:1
49
goroutine2 reader over
50
goroutine1 reader over
51
goroutine1 writing count:2
52
goroutine1 writer over
53
goroutine2 writing count:3
54
goroutine2 writer over
55
final count: 3
死锁
两个或以上的goroutine 在执行过程中,因争夺共享资源处在互相等待的状态,如果没有外部干涉将会一直处在阻塞状态。 这是称之为死锁。
Lock/Unlock 不成对
对锁进行拷贝使用
1
func main() {
2
var mu sync.Mutex
3
mu.Lock()
4
defer mu.Unlock()
5
copyMutex(mu)
6
}
7
func copyMutex(mu sync.Mutex) {
8
mu.Lock()
9
defer mu.Unlock()
10
fmt.Println("ok")
11
}
mu sync.Mutex 当作参数传给函数copyMutex , 锁进行了拷贝, 不是原来的锁变量了, 如果将带有锁结构的变量赋值给其他变量, 锁的状态会赋值, 多锁复制后的新锁拥有了原来的锁状态。
要避免锁拷贝, 并且保证Lock和Unlock 成对出现。
1
mu.Lock()
2
defer mu.Unlock()
循环等待
A等B, B等C, C等A
1
func main() {
2
var mu1, mu2 sync.Mutex
3
var wg sync.WaitGroup
4
5
wg.Add(2)
6
7
go func() {
8
defer wg.Done()
9
mu1.Lock()
10
11
defer mu1.Unlock()
12
time.Sleep(1 * time.Second)
13
14
mu2.Lock()
15
defer mu2.Unlock()
16
}()
17
18
go func() {
19
defer wg.Done()
20
mu2.Lock()
21
defer mu2.Unlock()
22
time.Sleep(time.Second)
23
mu1.Lock()
24
defer mu1.Unlock()
25
}()
26
wg.Wait()
27
}
28
两个goroutine, 一个goroutine 先锁mu1,再锁mu2, 另一个goroutine 先锁mu2 ,再锁mu1。 在进行第二次加锁的时候 会彼此等待对方释放锁,造成循环等待, 一直阻塞,形成死锁。
Sync.Map
Go 内置的map 并不是线程安全的, 在多个goroutine 同时操作mao的时候, 会有并发问题。
1
var m = make(map[string]int)
2
3
func getVal(key string) int {
4
return m[key]
5
}
6
7
func setVal(key string, value int) {
8
m[key] = value
9
}
10
11
func main() {
12
wg := sync.WaitGroup{}
13
wg.Add(10)
14
15
for i := 0; i < 10; i++ {
16
go func(num int) {
17
defer wg.Done()
18
key := strconv.Itoa(num)
19
setVal(key, num)
20
fmt.Printf("key=%v, val:=%v\n", key, getVal(key))
21
}(i)
22
}
23
wg.Wait()
24
}
25
26
27
//输出
28
fatal error: concurrent map writes
29