channel通道
- 前面有说过哈,channel是一种引用类型,make函数初始化以后才可以进行使用
chan1 := make(chan int)
chan2 := make(chan int, 10 )
- 带数字的表示拥有缓存区
带缓存区的通道和没有带缓存区的通道:
无缓存的通道必须要有人进行接收,有缓存的可以放入,满了以后会阻塞。
select关键字
- 同一时刻有多个通道要进行操作的场景呀,使用select来实现多路复用
func main() {
ch := make(chan int, 1)
for i := 1; i <= 10; i++ {
select {
case ch <- i:
case x := <-ch:
fmt.Println(x)
}
}
} // 答案: 1 3 5 7 9
- 这个答案是固定的哈,因为他的缓存区是1,进去一个,就不能进行了,只能等待
- 如果缓存区大一点,答案就是不可预测的了。
select语句可以提高代码的可读性:
- 可以处理一个或者多个channel的接收/发送操作
- 如果有多个case同时满足,select会随机选择一个
- 对于没有case操作的select{},会一直等待,可以用于阻塞main函数。
sync.WaitGroup的应用
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
jobChan := make(chan int, 100)
resultChan := make(chan int, 100)
// 不停的投入数据
go func() {
for i := 1; i <= 100; i++ {
jobChan <- i
time.Sleep(time.Millisecond * 100)
}
close(jobChan)
}()
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for v := range jobChan {
resultChan <- v
}
}()
}
go func() {
for i := range resultChan {
fmt.Println(i)
}
}()
wg.Wait()
close(resultChan)
}
- 除了sync.WaitGroup外,我们还可以利用通知来解决关闭的问题,因为不关闭,会导致程序不停的去读取值,从而导致死锁
notify通知使用,里面装空的匿名结构体
package main
import (
"fmt"
"time"
)
func main() {
jobChan := make(chan int, 100)
resultChan := make(chan int, 100)
notityChan := make(chan struct{}, 100)
// 不停的投入数据
go func() {
for i := 1; i <= 100; i++ {
jobChan <- i
time.Sleep(time.Millisecond * 100)
}
close(jobChan)
}()
for i := 0; i < 10; i++ {
go func() {
for v := range jobChan {
resultChan <- v
notityChan <- struct{}{}
}
}()
}
go func() {
for i := 0; i < 100; i++ {
<-notityChan
}
close(resultChan)
}()
for i := range resultChan {
fmt.Println(i)
}
}
- 使用匿名空结构体更方便哈,主要是不占空间,老舒服了,
- 也可以解决死锁的问题
互斥锁
- 多线程在一起运行的时候,可能会出现一些并发的错误。这个时候就需要用到互斥锁来解决了
- 来举个错误的例子
- 可以看到哈,答案只有13592,按理说应该是20000才对呀,为什么会有这么多数据无了呢,就是因为在并发的时候,两个公共的用户态线程去拿公共资源的时候发生了并发错误,导致了只拿到了一次。
- 那怎么解决呢?
互斥锁:
- 互斥锁是一种常用的控制公共资源访问的方法,它能够保证同时只有goroutine可以访问共享资源。
== 通过sync包里面的 Mutex
类型来实现互斥锁,用法也是相当简单。
lock.Lock() // 上锁
代码块
lock.Unlock() // 解锁
- 上锁后就不会出现并发错误啦。
读写互斥锁
- 上面的互斥锁是完全互斥的,但是有很多实际场景下是读多写少
- 我们的程序在并发读取一个资源的时候是不会有并发错误的,也就是是没有必要去加锁的,这种场景下,使用读写锁是更好的一种选择。
- 读写锁在go语言里面使用的是 sync 包里面的 RWMutex类型
读写锁分为两种: 读锁和写锁
当一个goroutine获取读锁的时候,其他goroutine如果获取读锁会继续获得锁,如果是获取写锁的时候就会进行等待;
当一个goroutine获取写锁的时候,其他goroutine不管的获取读锁还是写锁都会进入等待
- 这样操作就会比直接的互斥锁快上很多了哈
- 这里我们可以测试一下,设置一个公共资源x,先用互斥锁测试一次,再用读写测试一次
互斥锁程序测试
func read() {
defer wg.Done()
lock.Lock()
fmt.Println(x)
time.Sleep(time.Millisecond) // 假设读需要花费1毫秒
lock.Unlock()
}
func write() {
defer wg.Done()
lock.Lock()
x += 1
time.Sleep(time.Millisecond * 10) // 写需要花费10毫秒
lock.Unlock()
}
- 设置两个锁互斥锁
- 可以看到哈,写操作10次,读取操作1000的情况下,花费了1.8900291s
- 然后我们看一下读写锁哈,还记得吧,使用RWMutex类型哈
- 睁大眼睛看哦,只有 113.9215ms ,只有百来毫秒呀,兄弟们。
- 当读取次数越大的时候就越舒服,但是当写次数约大的时候也会越拉跨的。啊哈哈哈哈哈哈
- 因为读的太快了,公共资源都没加就读完了,emmmm这个要注意哈。
sync.Once
- 在编程的的很多场景下面我们需要确保某些操作在高并发的场景下面只执行一次,例如只加载一次配置文件、只关闭一次通道等等。
- 针对此类别的场景,go里面的提供了sync的Once类型
- 此类型里面只有一个 Do方法。里面需要传入一个函数
func (o *Once) Do(f func()) {}
- 如果需要强制进行运行的,那就需要使用闭包了。
程序自带的init方法,是当程序启动的时候,init也会跟着启动,但是有的时候我们并不需要这个初始化的操作,而他也进行了加载,这无形中消耗了我们的性能。
- 所以Once的优势就来了,当程序里面没有用到此类初始化的时候,他是不会被加载的,当用到的时候,他才会被程序进行加载。
你有可能会想到,直接加个if判断下是不是空再进行加载不久可以了!
- 是的,按理说串行的情况下确实没有问题,但是当并发的时候呢,就会被加载可能多次的情况。
- 而通过Once进行加载的话,他在保证用的时候才会加载的同时,也保证了线程的一个安全性。
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
- 这里给你整个,并发安全的案例模式。
sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
sync.map
- 首先我们要知道map在我们的日常开发中用的巨多,但是go里面的内置的map呢,他不是安全的,当出现高并发的时候就会出现很多并发的错误。
- 举个例子:
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
- 这里我们整一个普通map的读写操作
- 然后进行并发的去读取和写入,程序就直接炸了
这是因为:
go语言内置的Map,对于共享变量,资源,并发写会产生竞争, 共享资源遭到破坏
- 这个时候 sync.map 的作用就来了
- 这是Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map
- 开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
- 同步map里面内置的方法是可以直接使用的。舒服+1
一定要记住两个方法
m.Store(key, value) 进行存值
value, ok := m.Load(key) 用来进行读取数据
- 这两个一定记下来哈
原子操作
- 这个就是atomic包提供的操作哈,针对各种类型都有。
- 这个跟java里面那个atomic包差不多哒,提供一个变量的原子操作
什么是原子操作呢?
- 就是将一个变量的 读取、修改、赋值,看作成一个整体,要么全部成功,要么全部失败。
- 我们需要并发的本质也就是没有原子操作啦
使用也是很简单的,看官方文档走起
- 这里我们简单举个例子
- 给一个变量进行自加一次的简单操作,在高并发的时候都会出错,但是使用原子自加就不会了
atomic.AddInt64(&x,1)
- 这样就可以啦,很简单吧
go语言开发文档(中文版)
- 溜啦溜啦,要坚持哦!!!!