并发编程是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("主进程")
}
两个程序分别跑起来后我们发现如下区别:
- go的程序相对于python运行启动更快。
- go的程序对于CPU和内存的占用极为霸道,瞬间占满内存,直至主程序结束。而python对内存的占用则不会有太多增加。
- 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
以上的程序有什么问题呢?最大的问题是:
- 缺少协程控制。
- 生产消费不匹配,容易产生死锁,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)的死锁原因常见如下:
- 无缓冲的channel如果没有先启动一个消费者,存数据一定会死锁。
- 生产者消费者数量不匹配会产生死锁。
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的本质是一个接口,提供了几种线程控制的常用方法。