当前位置: 首页 > news >正文

Go语言之路————并发

Go语言之路————并发

  • 前言
  • 协程
  • 管道
  • Select
  • sync
    • WaitGroup

前言

  • 我是一名多年Java开发人员,因为工作需要现在要学习go语言,Go语言之路是一个系列,记录着我从0开始接触Go,到后面能正常完成工作上的业务开发的过程,如果你也是个小白或者转Go语言的,希望我这篇文章对你有所帮助。
  • 有关go其他基础的内容的文章大家可以查看我的主页,接下来主要就是把这个系列更完,更完之后我会在每篇文章中挂上连接,方便大家跳转和复习。

协程

在学go之前,大家肯定听说过go底层天然支持并发,相信这也是很多人选择学习这款语言的原因之一,那么它到底怎么个天然法,怎么个支持,下面我就一一道来。

Goroutine(轻量级线程),正如标题一样,它也叫做协程,它是go的并发执行单元,是一种比线程更加轻量级的单位,创建一个协程非常简单,只需要用到一个关键词:go,go后面一定要更一个函数:

func main() {go func() {fmt.Print(1)}()
}

我这里用一个go启动一个匿名函数,如果你copy这个代码去执行,你会发现控制台没有任何打印,因为协程就跟Java的线程一样,它是并发去执行的,当我们的main方法跑完的时候,如果协程未执行,那么 整个程序都会关掉,就没有任何输出了。

那怎样让它正常输出呢?聪明的同学肯定会想到,让main线程沉睡一下不就行了,我们来看看代码:

func main() {go func() {fmt.Print(1)}()time.Sleep(1 * time.Second)
}控制台打印:1

由此可见,让主线程沉睡确实可以做到这点,那么我就要提出下一个问题了,如果有多个协程呢?看看下面代码:

func main() {for i := 0; i < 10; i++ {go fmt.Println(i)}time.Sleep(1 * time.Second)
}

当把这段代码执行后,你会发现每次执行的结果都是不一样的,这也引出了协程的一个特性,那就是执行的时候是无序的,那有啥方法解决吗,我们先用上面的sleep看能否解决:
每次执行协程前,我们都让它沉睡一秒,然后主线程沉睡十秒

func main() {for i := 0; i < 10; i++ {time.Sleep(1 * time.Second)go fmt.Println(i)}time.Sleep(10 * time.Second)
}

执行后的结果:

0
1
2
3
4
5
6
7
8
9

目前来看,是做到了,但是这个方法太笨了,有啥办法可以优雅的解决吗,当然,go提供了管道、信号量、上下文、锁等各种工具来辅助开发者进行并发编程。

管道

管道:channel,官方对它的解释:Do not communicate by sharing memory; instead, share memory by communicating.
我用白话文在翻译一次:它的作用就是解决协程之间的通信的,数据传输或者共享的。
一个通道,用chan来定义,定义的时候必须要指定它存的数据类型:

var ch chan int

此时的管道还没初始化,是不能使用的,在go中,初始化一个管道,有且只有一个办法,那就是make关键词,make关键词提供一个额外参数:缓冲区

var ch = make(chan int, 1)

这里就是用make创建了一个缓冲区为1的管道,先看看使用:

func main() {var ch = make(chan int, 1)ch <- 1println(<-ch)
}
输出:1

结合例子,说一下管道的输出和输出:<-,没错就是用箭头表示,箭头的指向表示数据流向,a <- 1,表示把1发到a,<- a,表示从a读取数据

如何理解缓冲区:可以理解为Java中线程池中的阻塞队列,往管道中发送的数据会先存到缓冲区,然后才会被读取,如果一个管道没有缓冲区,那么发送信息后需要立马有读取的操作,否则程序就会阻塞,我们通过下面例子来看:

func main() {var ch = make(chan int)ch <- 1<-ch
}

我们创建一个没有缓冲区的管道,像管道里面输入1,马上再读取。看似人畜无害的代码,执行起来确是这个结果:deadlock

fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:
main.main()D:/goland/workspace/test/main.go:5 +0x2dProcess finished with the exit code 2

那读者又会想了,既然这样,那岂不是所有的管道创建都需要缓冲区。其实不然,如果我们通过协程去输入就能正常输出:

func main() {var ch = make(chan int)go func() {ch <- 1}()println(<-ch)
}
输出:1

思考:为啥有协程的参与就能正常读写?我们回到缓冲区的本质,它是存数据的缓冲的,如果我们没有缓冲区,那么证明这个管道是没办法存数据的,就意味着,我这边写了,必须马上有人读,但是通过同步操作是实现不了的,有协程异步来操作才可行。

注意:每个管道用完后需要我们手段关闭,直接调用系统提供的close方法,一个管道只能close一次,多次close会报错

func close(c chan<- Type)

但是通常,我们建议把通道的关闭结合defer来用:

func main() {var ch = make(chan int)go func() {ch <- 1defer close(ch)}()println(<-ch)
}

注意点,除了同步读写无缓冲管道会造成堵塞之外,下面几种情况也会造成deadlock:

  1. 缓冲区满了继续噻数据:
    func main() {var ch = make(chan int, 1)defer close(ch)ch <- 1ch <- 1println(<-ch)
    }
    
    缓冲区大小为1,写入一个后满了没读,继续写
  2. 有缓冲区,但是数据为空
    func main() {// 创建的有缓冲管道intCh := make(chan int, 1)defer close(intCh)// 缓冲区为空,阻塞等待其他协程写入数据<-intCh
    }
    
  3. 管道未初始化
    func main() {var intCh chan intintCh <- 1
    }
    

管道数据除了一个个读之外,我们还可以用for range来遍历一个管道:

func main() {intCh := make(chan int, 10)go func() {for i := 0; i < 10; i++ {intCh <- i}}()for ch := range intCh {println(ch)}
}

看看输出:

0
1
2
3
4
5
6
7
8
9
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:
main.main()D:/goland/workspace/test/main.go:10 +0xa8

在输出之后出现了阻塞,这是因为for range会一直去读写管道中的数据,当管道中数据为空时就会死锁,直到有其他协程向管道写入数据才会解除,所以我们代码改一下,在写入数据完毕后就关闭管道:

func main() {intCh := make(chan int, 10)go func() {for i := 0; i < 10; i++ {intCh <- i}close(intCh)}()for ch := range intCh {println(ch)}
}

最后再补充一个知识点,管道的读取其实是有返回值的:

v, ok := <-intCh

第一个是值,第二个是个bool代表是否读取成功:

func main() {intCh := make(chan int, 10)go func() {intCh <- 1}()a, ok := <-intChprintln(a, ok)
}输出:1 true

Select

在 Go 中,select 是一种管道多路复用的控制结构,某一时刻,同时监测多个元素是否可用,在这里我们可以用来检测多个管道:

func main() {ch1 := make(chan int, 10)ch2 := make(chan int, 10)ch3 := make(chan int, 10)defer func() {close(ch1)close(ch2)close(ch3)}()select {case i := <-ch1:fmt.Println("ch1 is ", i)case j := <-ch2:fmt.Println("ch2 is ", j)case k := <-ch3:fmt.Println("ch3 is ", k)default:fmt.Print("检测失败")}
}

创建三个管道,然后用select分别去监测三个管道的数据,然后doSomething,让我们没有往管道输入任何数据的时候,默认输出检测失败,我们在select前往ch1输入一个数据看看:

func main() {ch1 := make(chan int, 10)ch2 := make(chan int, 10)ch3 := make(chan int, 10)defer func() {close(ch1)close(ch2)close(ch3)}()ch1 <- 1select {case i := <-ch1:fmt.Println("ch1 is ", i)case j := <-ch2:fmt.Println("ch2 is ", j)case k := <-ch3:fmt.Println("ch3 is ", k)default:fmt.Print("检测失败")}
}输出:ch1 is  1

sync

讲到了并发,怎么能离开锁,go的sync包下面提供了很多锁相关的工具类,就类似于Java的juc包,我们下面简单说点常用的。

WaitGroup

WaitGroup 即等待执行,它的方法只有三个,使用起来也非常简单:

  • Add:添加一个计数器,表示总数
  • Done:每调用一次计数器减1
  • Wait:如果计数器不为0,则等待

还记得我们文章开头提到的例子吗,就是在main线程中使用了协程,协程还未执行但是main已经结束了,当时我们用的是sleep方法,现在我们看看怎么用WaitGroup去解决这个问题:
先看看原例子:

func main() {println("start")go func() {println("doSomething")}()println("end")
}

再看看解决后的:

var waitGroup sync.WaitGroupfunc main() {println("start")waitGroup.Add(1)go func() {println("doSomething")waitGroup.Done()}()waitGroup.Wait()println("end")
}看看输出:
start
doSomething
end

go中常用的锁有两个:

  • 互斥锁:sync.Mutex
  • 读写锁:sync.RWMutex

互斥锁sync.Mutex ,实现了Locker 接口,它的用法非常简单,就三个:

func (m *Mutex) Lock() {m.mu.Lock()
}func (m *Mutex) TryLock() bool {return m.mu.TryLock()
}func (m *Mutex) Unlock() {m.mu.Unlock()
}

我们先来看看互斥锁Mutex,下面我来模拟一个经典的场景,就是不同线程对共享数据操作,让我们看看不用锁的情况下,会不会得到正确结果:

var wait sync.WaitGroup
var count = 0func main() {wait.Add(10)for i := 0; i < 10; i++ {go func(data *int) {// 模拟访问耗时time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))// 访问数据,这里必须要用temp当前数据存起来temp := *data// 模拟计算耗时time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))// 修改数据*data = temp + 1fmt.Println(*data)wait.Done()}(&count)}wait.Wait()fmt.Println("最终结果", count)
}

运行起来发现,每次的输出都不一样,跟Java一样,多线程对共享数据的修改是不安全的,必须要加锁

1
1
2
1
1
1
1
1
1
3
最终结果 3

下面我们改进一下代码,将同步代码用互斥锁包起来,类似于Java的同步代码块:

var lock sync.Mutex
var wait sync.WaitGroup
var count = 0func main() {wait.Add(10)for i := 0; i < 10; i++ {go func(data *int) {lock.Lock()// 模拟访问耗时time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))// 访问数据temp := *data// 模拟计算耗时time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))// 修改数据*data = temp + 1lock.Unlock()fmt.Println(*data)wait.Done()}(&count)}wait.Wait()fmt.Println("最终结果", count)
}

go的互斥锁很简单,用的时候就调用lock()方法,解锁就调用unlock()方法,看看输出:

1
2
3
4
5
6
7
8
9
10
最终结果 10Process finished with the exit code 0

读写锁和互斥锁一样,只是说读写锁的精度更高一点,可以根据读多写少,或者读少写多的情况来判断,它同样实现了Locker接口,只是方法多一些,读写锁内部的读和写是互斥锁,并不是说有两个锁

// 加读锁
func (rw *RWMutex) RLock()// 尝试加读锁
func (rw *RWMutex) TryRLock() bool// 解读锁
func (rw *RWMutex) RUnlock()// 加写锁
func (rw *RWMutex) Lock()// 尝试加写锁
func (rw *RWMutex) TryLock() bool// 解写锁
func (rw *RWMutex) Unlock()

下面看个读写锁的例子(本例来自官方中文文档):

var wait sync.WaitGroup
var count = 0
var rw sync.RWMutexfunc main() {wait.Add(12)// 读多写少go func() {for i := 0; i < 3; i++ {go Write(&count)}wait.Done()}()go func() {for i := 0; i < 7; i++ {go Read(&count)}wait.Done()}()// 等待子协程结束wait.Wait()fmt.Println("最终结果", count)
}func Read(i *int) {time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))rw.RLock()fmt.Println("拿到读锁")time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))fmt.Println("释放读锁", *i)rw.RUnlock()wait.Done()
}func Write(i *int) {time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))rw.Lock()fmt.Println("拿到写锁")temp := *itime.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))*i = temp + 1fmt.Println("释放写锁", *i)rw.Unlock()wait.Done()
}

该例开启了 3 个写协程,7 个读协程,在读数据的时候都会先获得读锁,读协程可以正常获得读锁,但是会阻塞写协程,获得写锁的时候,则会同时阻塞读协程和写协程,直到释放写锁,如此一来实现了读协程与写协程互斥,保证了数据的正确性。例子输出如下:

拿到读锁
拿到读锁
释放读锁 0
释放读锁 0
拿到写锁
释放写锁 1
拿到读锁
拿到读锁
拿到读锁
拿到读锁
拿到读锁
释放读锁 1
释放读锁 1
释放读锁 1
释放读锁 1
释放读锁 1
拿到写锁
释放写锁 2
拿到写锁
释放写锁 3
最终结果 3Process finished with the exit code 0

OK 上面就是go中并发的一些常用案例,不多,但是一定是最常用的,掌握了这些你就可以去深入扩展了。

相关文章:

  • 一键清理功能,深度扫描本地存储数据
  • 深度学习驱动下的目标检测技术:原理、算法与应用创新(三)
  • memcached主主复制+keepalive
  • Python多线程实战:提升并发效率的秘诀
  • Linux常用命令42——tar压缩和解压缩文件
  • Python 之类型注解
  • Java项目使用Tomcat启动后JS文件中的中文乱码问题
  • 彻底删除Docker容器中的环境变量
  • 【Win32 API】 lstrcmpA()
  • 第J1周:ResNet-50算法实战与解析
  • entity线段材质设置
  • let、var、const的区别
  • 基于javaweb的SSM驾校管理系统设计与实现(源码+文档+部署讲解)
  • 软考第六章知识点总结
  • 如何安装cuda版本的pytorch
  • PTN中的L2VPN与L3VPN技术详解
  • 时频分析的应用—外部信号的显影和定点清除
  • LLM笔记(七)注意力机制
  • WL-G4048 Multi-Port PCIe 4.0 Switch
  • 学习状态不佳时的有效利用策略
  • 北京韩美林艺术馆党支部书记郭莹病逝,终年40岁
  • 贞丰古城:新垣旧梦间的商脉与烟火
  • 中国军网:带你揭开3所新调整组建军队院校的神秘面纱
  • 上海市重大工程一季度开局良好,多项生态类项目按计划实施
  • 菲律宾中期选举初步结果出炉,杜特尔特家族多人赢得地方选举
  • 七部门:进一步增强资本市场对于科技创新企业的支持力度