并发编程是go最大的特征。在python、Java、C++的并发编程主要是多进程和多线程的开发。但是每个线程占用的内存较多,系统开销大。因为web2.0的高并发使得单靠线程进行并发变得很不经济。

go的协程初识

go的协程开启方式十分简单,使用关键字go即可。

程序示例:

package main

import (
	"fmt"
	"time"
)

func p(n int){
	fmt.Printf("协程%d\n", n)
}

func main() {
	for i := 0; i < 3; i++ {
		go p(i)	// 开启协程
	}
	// 因为有主死从随,所以开启延时可以保证协程的执行
	time.Sleep(time.Second*2)
	fmt.Println("主进程")
}

运行结果:

协程2 协程0 协程1 主进程

对比python的协程

python在3.6以后也有了协程的概念,python的Tornada和sanic框架也使用到了协程。

我们以python和go同时开启一百万个协程,看运行效果进行对比。

python程序示例:
# python程序

# _*_ coding:utf-8 _*_
__author__ = 'wulian'
__date__ = '2022/4/13 0013 15:05'

import asyncio

async def p(n):
    while 1:
        print(n)
        # python中的协程使用sleep必须使用asyncio中的sleep
        await asyncio.sleep(1)

async def main():
    for i in range(1000000):
        asyncio.create_task(p(i))
    await asyncio.sleep(10000)

asyncio.run(main())
go程序示例:
package main

import (
	"fmt"
	"time"
)

func p(n int){
	fmt.Printf("协程%d\n", n)
	time.Sleep(time.Second)

}

func main() {
	for i := 0; i < 1000000; i++ {
		go p(i)	// 开启协程
	}
	// 因为有主死从随,所以开启延时可以保证协程的执行
	time.Sleep(time.Second*1000)
	fmt.Println("主进程")
}

两个程序分别跑起来后我们发现如下区别:

  1. go的程序相对于python运行启动更快。
  2. go的程序对于CPU和内存的占用极为霸道,瞬间占满内存,直至主程序结束。而python对内存的占用则不会有太多增加。
  3. go的执行速度更快

 waitgroup控制协程

上面的程序中我们直接采用sleep函数来延缓主程序的结束以保证从协程能够顺利执行,但是有明显得缺点:sleep时间过短,从协程无法全部执行,sleep时间过长,从协程执行完毕,程序仍然会大量占用内存直至sleep时间结束。有没有方法能够保证从协程执行完主程序就结束呢?

有的,这个就是go的内置WaitGroup.,他提供了三个函数Add/ Down/ Wait保证程序的运行。

程序示例:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	for i := 0; i < 1000000; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			fmt.Println(n)
		}(i)
	}
	wg.Wait()
	// 因为有主死从随,所以开启延时可以保证协程的执行
	//time.Sleep(time.Second*10)	// 无法保证10S内从线程执行完
	// go提供waitgroup保证从线程执行完主线程结束

	fmt.Println("主进程")
}

使用互斥锁同步协程

并发编程最大的问题是什么?资源竞争。

解决资源竞争问题就需要用到锁的概念。

如下的程序用到了互斥锁保证了加函数和减函数不会无序竞争,可以得到正确的计算结果。如果给两个函数没有加锁,那么计算结果将是不确定的。

程序示例:

package main

import (
	"fmt"
	"sync"
)

var total int
var wg sync.WaitGroup
var lock sync.Mutex		// 互斥锁

func add(){
	defer wg.Done()
	for i := 0; i<1000000; i++ {
		lock.Lock()	// 加锁
		total += i
		lock.Unlock()	// 释放锁
	}
}

func sub(){
	defer wg.Done()
	for i := 0; i<1000000; i++ {
		lock.Lock()	// 加锁
		total -= i
		lock.Unlock()	// 释放锁
	}
}

func main() {
	wg.Add(2)
	go add()
	go sub()
	wg.Wait()
	fmt.Println(total)
}
运行结果:
0

使用读写锁

上面的互锁解决了资源的无序竞争问题,但是互斥锁明显会使系统性能下降。对于正常的web系统来说,一般读多写少,如果都使用互斥锁,并发就会严重下降。我们采取读写锁提高并发。

如下的程序示例我们采用了读写锁,可以看出读可以并发进行。但是写的操作一定是互斥的。

程序示例:

package main

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

var wg sync.WaitGroup
var rwLock sync.RWMutex		// 读写锁

func read(){
	defer wg.Done()
	rwLock.RLock()		// 读锁
	fmt.Println("开始读数据···")
	time.Sleep(time.Second)
	fmt.Println("读数据成功···")
	rwLock.RUnlock()
}

func write(){
	defer wg.Done()
	rwLock.Lock()	// 写锁
	fmt.Println("开始修改数据···")
	time.Sleep(time.Second*2)
	fmt.Println("修改成功···")
	rwLock.Unlock()
}

func main() {
	wg.Add(7)
	for i := 0; i<5; i++{
		go read()
	}
	for i := 0; i<2; i++{
		go write()
	}
	wg.Wait()
}

运行结果:

开始读数据··· 开始读数据··· 开始读数据··· 读数据成功··· 读数据成功··· 读数据成功··· 开始修改数据··· 修改成功··· 开始读数据··· 开始读数据··· 读数据成功··· 读数据成功··· 开始修改数据··· 修改成功···


线程间通信channel

channel提供了线程间的通信,是一个消息队列。我们先用一个例子来了解channel的定义、实例化、存值、取值方法。

程序示例:

package main

import "fmt"

func main() {
	// channel提供了一种通信机制,定向,python,java,消息队列
	var msg chan int	// channel管道的定义,属于引用类型,需要初始化

	// go语言中slice/ map/ channel三种类型使用make初始化
	// 初始化方式1:无缓冲空间,会产生死锁
	//msg = make(chan int)

	// 初始化方式2:有缓冲空间,缓冲空间要和需要存入的空间匹配,否则也会死锁
	msg = make(chan int, 1)

	msg <- 1	// 将值放入到channel

	data := <- msg	// 取值

	fmt.Println(data)
}

运行结果:

1

以上的程序有什么问题呢?最大的问题是:

  1. 缺少协程控制。
  2. 生产消费不匹配,容易产生死锁,python中生成大于消费会阻塞,但是go中生成大于消费就会死锁,而死锁是必须避免的。

协程控制示例:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func consumer(queue chan int)  {
	defer wg.Done()
	data := <- queue
	fmt.Println(data)
}
func main() {
	// 1.定义channel
	var msg chan int	// channel管道的定义,属于引用类型,需要初始化

	// 初始化方式2:有缓冲空间,缓冲空间要和需要存入的空间匹配,否则也会死锁
	msg = make(chan int, 1)

	msg <- 1	// 将值放入到channel
	wg.Add(1)
	go consumer(msg)
	wg.Wait()

}

运行结果:

1

上面的程序仍然有缺陷,无法判定管道是否关闭。

程序示例:

package main

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

var wg sync.WaitGroup

// 消费者
func consumer(queue chan int)  {
	defer wg.Done()
	// 无法确定channel是否关闭
	//for data := range queue{
	//	fmt.Println(data)
	//}
	for {
		data, ok := <- queue
		if !ok {
			break
		}
		fmt.Println(data)
		time.Sleep(time.Second)
	}
}
func main() {
	// 1.定义channel
	var msg chan int	// channel管道的定义,属于引用类型,需要初始化

	// 初始化方式2:有缓冲空间,缓冲空间要和需要存入的空间匹配,否则也会死锁
	msg = make(chan int, 1)

	msg <- 1	// 将值放入到channel
	wg.Add(1)
	go consumer(msg)
	msg <- 2	//
	// 关闭channel,
		//	1.已经关闭的channel不能再发送数据了
		//	2.已经关闭的channel能够继续取数据,直到数据取完为止
	close(msg)
	wg.Wait()

}

运行结果:

1 2


 双向和单向的channel

go语言的channel除了有缓冲和无缓冲的区别外,还有单向和双向的区别。之前的channel即可放值也可以取值,属于双向channel。

下面是一个双向管道使用无缓冲的示例:

程序示例:

package main

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

var wg sync.WaitGroup

// 消费者
func consumer(queue chan int)  {
	defer wg.Done()
	for {
		data, ok := <- queue
		if !ok {
			break
		}
		fmt.Println(data)
		time.Sleep(time.Second)
		queue <- 2
	}
}
func main() {
	var msg chan int	// channel管道的定义,属于引用类型,需要初始化
	msg = make(chan int)	// 无缓冲模式需要先有消费者
	wg.Add(1)
	go consumer(msg)	// 消费者先于生成者
	msg <- 1	// 将值放入到channel
	fmt.Println(<-msg)
	close(msg)
	wg.Wait()

}

运行结果:

1 2


 单向channel的定义方式如下,我们一般定义生产者为放值的channel,消费者为取值的channel。双向channel可以转为单向channel,反之不行。

var msg chan<- int	// 定义只能放值的单向channel

	var msg <-chan int	// 定义只能取值的单向channel

channel的deadlock原因

消息队列(管道channel)的死锁原因常见如下:

  1. 无缓冲的channel如果没有先启动一个消费者,存数据一定会死锁。
  2. 生产者消费者数量不匹配会产生死锁。

select的应用场景

go语言提供了一个select的功能,作用于channel之上。解决多个channel的选择问题,处理多路复用问题。

程序示例:

package main

import (
	"fmt"
)

func main() {
	ch1 := make(chan int, 1)
	ch2 := make(chan int, 1)
	ch1 <- 1
	ch2 <- 2

	// select会公平随机的选择执行管道取值
	select {
	case data := <- ch1:
		fmt.Println(data)
	case data := <- ch2:
		fmt.Println(data)
	}
}

运行结果:

1或者2不定


 select应用场景:timeout超时机制和执行控制

无default的时候上面的case随机执行,但是有default之后只执行default中内容

程序示例:

package main

import (
	"fmt"
	"time"
)

func main() {
	timeout1 := make(chan bool, 2)
	go func() {
		time.Sleep(time.Second*5)
		timeout1 <- true
	}()

	timeout2 := make(chan bool, 2)
	go func() {
		time.Sleep(time.Second*2)
		timeout2 <- true
	}()

	select {
		case <- timeout1:
			fmt.Println("超时了3")
		case <- timeout2:
			fmt.Println("超时了1")
		default:    // 无default的时候上面的case随机执行,但是有default之后只执行default中内容
			fmt.Println("继续下一次")
	}
}

运行结果:

继续下一次


context应用场景

go的1.7版本以后有了context机制。使用select + context的方式或者select + channel的方式都可以在协程执行的适时退出。context在web开发中非常常用,grpc每个接口调用都会传递context gin的http接口也会有context。

context的本质是一个接口,提供了几种线程控制的常用方法。