go并发实践 | 青训营笔记

82 阅读9分钟

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,且一一对应。

12
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) {
78
      defer func() {
9
        //协程内部捕获panic
10
        if e := recover(); e != nil {
11
          fmt.Printf("recover:%v\n", e)
12
        }
1314
        wg.Done()
15
      }()
1617
      e := handler() // 真正的逻辑调用
18
      // 取第一个报错的handler 调用的错误返回
19
      if err == nil && e != nil {
20
        err = e
21
      }
22
    }(opt) //将goroutine的函数逻辑通过封装成的函数变量传入
23
  }
24
  wg.Wait() //等待所有的协程执行完
25
  return
26
}
2728
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
  }
3738
  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
34
//声明管道,需要进行初始化为其分配空间,
5
channel_name = make(chan channel_type)
6
channel_name = make(chan channel_type, size) //带有缓存的管道,size为缓存大小
78
//或者一步完成
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() {
23
  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() {
23
  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
// 结果
2223
v=1
24
v=2
25
v=3
26
v=4
27
channel数据已读完

for range读取

1
func main() {
23
  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
567
//单向写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
34
func main() {
56
  var ch = make(chan int)
7
  go func() {
8
    var send SChannel = ch
9
    fmt.Println("send:100")
10
    send <- 100
11
  }()
1213
  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
78
}
910
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
//输出
2122
-5 17 12

channel 分为两类: 有缓冲channel 和 无缓冲channel 。

为了协程安全, 不管有无缓冲channel , 内部都会有一把锁 来控制并发访问。

channel 底层有一个队列,来存储数据。

无缓冲 channel 可以理解为 同步模式, 写入一个消息, 如果没有消费者消费,写入就回阻塞

有缓冲channel 可以理解为 异步模式, 写入消息,如果没有被消费,只要队列没满,可以继续写入。

download_image

如果缓冲channel队列满了,发送就回阻塞。 download_image (1)

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)
89
  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
  }
910
  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
23
func myGoroutine() {
4
  defer wg.Done()
5
  fmt.Println("myGoroutine!!")
6
}
78
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 {}
34
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
23
func add1() {
4
  num += 1
5
}
67
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
23
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
1011
}
1213
func main() {
14
  var wg sync.WaitGroup
15
  var mu sync.Mutex
1617
  wg.Add(10) //开启10个goroutine 计数器加10
1819
  for i := 0; i < 10; i++ {
20
    go add1(&wg, &mu)
21
  }
22
  wg.Wait() //等待所有协程执行完
2324
  fmt.Println(num)
25
}
2627
// 输出
28
10

读写锁RWMutex

读写锁就是将读操作和写操作 分开, 分别对读和写加锁, 一般用在大量读操作, 少量写操作的时候。

1
func (rw *RWMutex) Lock() //对写操作加锁
2
func (rw *RWMutex) Unlock() //对写操作解锁
34
func (rw *RWMutex) RLock() //对读操作加锁
5
func (rw *RWMutex) RUnlock() //对读操作解锁
6

需要遵循几个守则:

  1. 同时只能有一个goroutine能够获得写锁定
  2. 同时可以有 任意多个goroutine 获得读锁定
  3. 同时只能存在写锁定或读锁定(读和写互斥)
1
ar cnt int
23
func main() {
4
  var mr sync.RWMutex
5
  for i := 1; i <= 3; i++ {
6
    go write(&mr, i)
7
  }
89
  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
}
1516
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()
2223
  fmt.Printf("goroutine%d reader over\n", i)
24
}
2526
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
//输出
3637
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
45
  wg.Add(2)
67
  go func() {
8
    defer wg.Done()
9
    mu1.Lock()
1011
    defer mu1.Unlock()
12
    time.Sleep(1 * time.Second)
1314
    mu2.Lock()
15
    defer mu2.Unlock()
16
  }()
1718
  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)
23
func getVal(key string) int {
4
  return m[key]
5
}
67
func setVal(key string, value int) {
8
  m[key] = value
9
}
1011
func main() {
12
  wg := sync.WaitGroup{}
13
  wg.Add(10)
1415
  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
}
252627
//输出
28
fatal error: concurrent map writes
29