# go异步编程

​ go作为一门高性能的服务器语言,除了语法简单之外,异步能力也是非常强悍,go中弱化了线程的概念,引入了协程。

​ 协程是一个用户级别的异步编程模型,它运行再线程之上。线程由操作系统调度,而协程由go引擎自己调度,所以可以避免线程的切换而提高整体性能。

# go同步编程包

# sync.Locker 同步锁接口约定

go异步包约定要实现锁功能需要满足Locker接口的确定

type Locker interface {
	Lock()
	Unlock()
}
1
2
3
4

# sync.Mutex 互斥锁,解决协程同步问题

var mutex sync.Mutex
var index int
func main() {
	for i := 0; i < 10; i++ {
		go addIndex()
	}
}
func addIndex() {
	// 使用锁,保证一个时刻只能一个协程修改index的值
	mutex.Lock()
	defer mutex.Unlock()
	index++
	log.Println(index)
	time.Sleep(time.Second)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# sync.RWMutex 读写锁,解决多读少写的协程同步场景

读写锁有两种情况:

  1. 加读锁后,所有的写锁都需要排队
  2. 加写锁后,效果跟互斥锁一样

下面是两种情况的演示示例

# 下面代码我没有使用time.Sleep来做延迟执行,减少代码的复杂度
var mutex sync.RWMutex
var index int
func main() {
	# 首先,先来两个读写
	go readIndex()
	go readIndex()
	// 延迟1秒保证读锁先执行
	time.Sleep(time.Second)
	// 遇见写锁后,所有的读锁都需要排队
	go addIndex()
	// 延迟一秒保证写锁生效
	time.Sleep(time.Second)
	// 读锁必须等待写锁完成才会执行
	go readIndex()
	time.Sleep(time.Minute)
}
func addIndex() {
	mutex.Lock()
	defer mutex.Unlock()
	index++
	log.Println("update:", index)
	time.Sleep(time.Second * 5)
}
func readIndex() {
	mutex.RLock()
	defer mutex.RUnlock()
	log.Println("reader:", index)
	time.Sleep(time.Second * 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
25
26
27
28
29
30
31
32
33

# sync.Once 仅首次执行

无论Do了多少个方法,只有第一个会被执行

func main() {
	once := sync.Once{}
	for i := 0; i < 5; i++ {
		once.Do(func() {
			log.Println("once:", i)
		})
	}
	time.Sleep(time.Second)
}
1
2
3
4
5
6
7
8
9

# sync.Cond 条件同步锁,降低同步锁的控制权,转由自己负责

当我们创建一个条件锁时,要先关联一个Locker对象,然后将这个Locker对象锁的控制权接管过来,也就是Lock不再作为协程的阻塞功能,而是标记是否获得了锁,而阻塞功能有sync.Cond的Wait方法实现,这样做的好处是:可以将锁的控制权提升到当前协程之外,如下代码:

var cond = sync.NewCond(&sync.Mutex{})
var index int
func main() {
	// 首先按我们启动10个goroutine
	for i := 0; i < 10; i++ {
		go AddAge()
	}
	for {
		// 所有的goroutine执行完成后跳出循环,结束程序
		if index == 10 {
			break
		}
		// 每隔1s我们允许一个goroutine修改index的值
		cond.Signal()
		// 你也可以一次性通知所有等待的goroutine
		// cond.Broadcast()
		time.Sleep(time.Second)
	}
}
func AddAge() {
	// 将sync.Mutex关联sync.Cond后,只有拿到锁才能加入Wait队列
	// 没有得到锁的goroutine将会被忽略
	cond.L.Lock()
	defer cond.L.Unlock()
	log.Println("get lock")
	cond.Wait()
	log.Println("wait")
	index++
	log.Println(index)
}
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

# sync.WaitGroup 协程组阻塞器,等待一组协程全部完成

sync.WaitGroup运行你创建一组协程并等待执行完成

func main() {
	group := sync.WaitGroup{}
	for i := 0; i < 10; i++ {
		go func(index int) {
			// 方法完成时调用Done告诉group完成了,然后将成员数减1
			defer group.Done()
			// 每次有新的goroutine启动都将group成员数加1
			group.Add(1)
			time.Sleep(time.Duration(int(time.Second) * index))
			log.Println("complete:", index)
		}(i)
	}
	// 阻塞直到group中goroutine全部完成
	group.Wait()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# sync.Pool 临时对象池,降低垃圾回收几率,减少内存申请次数

临时对象池是一组具有相同类型的对象集合,它由Put方法添加或者在Get方法的时候由New方法创建。

它可以避免多次申请新的内存而导致的不必要垃圾回收,而且它是线程安全的,go直到什么时候该给我们创建新对象,什么时候该使用就对象,什么时候该清理对象池。

type People struct {
	Name string
	Age  int
}
// 对象使用完成后清空对象到初始状态
func (p *People) ResetPeople() {
	p.Name = ""
	p.Age = 0
}
var index int
func main() {
	pools := sync.Pool{New: createPeople}
	for i := 0; i < 10; i++ {
		// 使用对象池
		people := pools.Get().(*People)
		people.Name = fmt.Sprintf("name%d", i)
		people.Age = i
		people.ResetPeople()
		pools.Put(people)
	}
	log.Println("===============我是华丽的分割线==================")
	index = 0
	for i := 0; i < 10; i++ {
		// 不使用对象池
		createPeople()
	}
}
func createPeople() interface{} {
	index++
	log.Println("create people 次数:", index)
	return &People{}
}
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

上面例子可以清晰的看到,当我们使用对象池的时候创建People对象的次数少于10次,而不适用对象池是创建了对象10次

# go原子递增

在不使用原子递增的情况下,我们会使用互斥锁来修改一个对象的值

当使用了原子递增时我没有必要使用互斥锁实修改对象的值,而且还能获取更好的性能

下面简单例子,其它api请查看官方文档

func main() {
	var count int32
	for i := 0; i < 100000; i++ {
		atomic.AddInt32(&count, 1)
	}
	log.Println("count:", count)
}
1
2
3
4
5
6
7

# go并行

go默认已经开启了并行计算,不需要我们手动开启

go自动确定使用的cpu核心数,我们可以手动指定核心数

# 获取核心数
runtime.NumCPU()
# 设置核心数
runtime.GOMAXPROCS(6)
1
2
3
4