go-doc-并发

基础概念

  1. 并发:多线程程序在单核上运行
  2. 并行:多线程程序在多核上运行
  3. go1.8之前,go程序的cpu数需要手动设置。1.8之后默认让程序运行在多核上,可以不设置
    https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/

    goroutine

Go协程特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程(编译器做优化)

协程注意点

  1. 主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源
  2. 协程从主线程开启的,是轻量级的,是逻辑太,对资源消耗相对小
  3. 如果主线程退出了,则协程即使还没有执行完毕,也会退出
  4. go的协程机制是重要的特点,可以轻松的开启上完个协程。其他编程语言的并发机制一般是基于现场,开启过多的线程,资源消费大

错误处理

goroutine发生错误的时候,通过panic和recover来解决。如果不处理会导致整个程序崩溃

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

import (
"fmt"
"time"
)

func test() {
defer func() {
if err := recover(); err != nil {
fmt.Println("test发生错误", err)
}
}()

var map1 map[int]string
map1[0] = "1111"
}

func main() {

go test()

for {
fmt.Println("....")
time.Sleep(time.Second)
}
}

MPG调度模型

  • M:操作系统的线程(物理线程)
  • P:协程执行需要的上下文
  • G:协程

channel

  • 基本语法
1
var 变量名 chan 数据类型

为什么需要channel

  1. 主线程在等待所有goroutine全部完成的时间很难确定,仅仅是估算
  2. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处理工作状态,这时也会随主线程的退出而销毁
  3. 通过全局变量加锁同步实现通讯,也并不利于多个协程对全局变量的读写操作

channel要点

  1. channel本质就是一个数据结构-队列
  2. 数据是先进先出
  3. 线程安全,多个goroutine访问时,不需要加锁
  4. channel是有类型的,一个string的channel只能存放string类型数据
  5. channel是引用类型
  6. channel必须初始化才能写入数据,即make后才能使用
  7. 当给channel写入数据时,不能超过容量,不然会报错,也就是不会动态扩展
  8. 当没有channel没有数据时,进行取数据会阻塞
  9. 如果想放任意类型,可以使用interface类型,但是不建议
  10. channel被关闭后,只能读不能写
  11. channel可以声明为只可读或只可写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main 

import (
"fmt"
)

// 函数指定只读
func recv(ch <-chan int){

}

// 函数指定只写
func send(ch chan<- int) {

}

func main() {

// 创建channel
var intChan chan int
intChan = make(chan int, 3)

// 查看channel信息
fmt.Printf("intChan=%v addr=%p\n", intChan, &intChan)

// 向管道写入数据
intChan<- 10
num := 211
intChan<- num

// 打印
fmt.Printf("intChan len=%v cap=%v\n", len(intChan), cap(intChan))

// 从管道取数据
var num2 int
num2 = <-intChan

fmt.Println(num2)
fmt.Printf("intChan len=%v cap=%v\n", len(intChan), cap(intChan))

// 关闭管道
close(intChan)

// 只读
var readChan <-chan int;
// 只写
var writeChan chan<- int;
}

channel遍历要点

  1. channel支持for-range进行遍历
  2. 在遍历时,如果channel没有关闭,则会出现deadlock的错误
  3. 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main 

import (
"fmt"
"time"
)

var intChan chan int

// 随眠5秒关闭管道,不然主线程会死锁
func closeChan() {
time.Sleep(time.Second * 5)
close(intChan)
}

func main() {

intChan = make(chan int, 100)
for i := 0; i < 100; i++ {
intChan<- i * 2
}

var sec1 = time.Now().Unix()

go closeChan()

for v := range intChan {
fmt.Println("v=", v)
}

var sec2 = time.Now().Unix()

fmt.Println("等待时间", sec2 - sec1)
}

select

在读取没有关闭的channel的时候,如果没有数据,会一直死锁,可以通过select来解决。

select要点

  1. select不能直接用break跳出,只能通过标签或return跳出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main 

import (
"fmt"
)

func main() {

intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}

stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan<- fmt.Sprintf("hello %d", i)
}

// select处理
label1:
for {
select {
// 如果读取不到数据,不会deadlock,而是会跳到下一个case
case v := <-intChan:
fmt.Printf("从intChan读到数据%v\n", v)
case v := <-stringChan:
fmt.Printf("从stringChan读到数据%v\n", v)
default:
fmt.Println("都读取不到数据")
break label1
}
}

close(intChan)
close(stringChan)

fmt.Println("退出")
}

数据交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main 

import (
"fmt"
)

func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}

fmt.Printf("readData 读到数据 %v\n", v)
}

exitChan<- true
close(exitChan)
}

func writeData(intChan chan int) {
for i := 1; i < 50; i++ {
intChan<- i
fmt.Printf("writeData 写入数据 %v\n", i)
}

close(intChan)
}

func main() {

intChan := make(chan int, 50)
exitChan := make(chan bool, 1)

go writeData(intChan)
go readData(intChan, exitChan)

// 等待操作完成
for {
_, ok := <-exitChan
if !ok {
break
}
}

fmt.Println("读写操作完成!!")
}

Cond

Cond实现了一种条件变量,主要用来解决多个读协程等待共享资源变成ready的场景。在使用Cond的时候,需要特别注意下:每个Cond都会关联一个Lock(*sync.Mutex or *sync.RWMutex),当修改条件或者调用Wait方法时,必须加锁,保护condition。

Map

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

import (
"fmt"
"sync"
)

func main() {
var scene sync.Map

// 将键值对保存到sync.Map
scene.Store("greece", 97)
scene.Store("london", 100)
scene.Store("egypt", 200)

// 从sync.Map中根据键取值
fmt.Println(scene.Load("london"))

// 根据键删除对应的键值对
scene.Delete("london")

// 遍历所有sync.Map中的键值对
scene.Range(func(k, v interface{}) bool {
fmt.Println("iterate:", k, v)
return true
})
}