GO通道和 sync 包的分享

[TOC]shell

GO通道和 sync 包的分享

咱们一块儿回顾一下上次分享的内容:安全

  • GO协程同步若不作限制的话,会产生数据竞态的问题
  • 咱们用锁的方式来解决如上问题,根据使用场景选择使用互斥锁 和 读写锁
  • 比使用锁更好的方式是原子操做,可是使用go的 sync/atomic须要当心使用,由于涉及内存

要是对GO的锁和原子操做还感兴趣的话,欢迎查看文章GO的锁和原子操做分享数据结构

上次咱们分享到锁和原子操做,均可以保证共享数据的读写闭包

但是,他们仍是会影响性能,不过,Go 为开发这提供了 通道 这个神器并发

今天咱们来分享一下Go中推荐使用的其余同步方法,通道和 sync 包ide

通道是什么?

是一种特殊的类型,是链接并发goroutine的管道函数

channel 通道是可让一个 goroutine 协程发送特定值到另外一个 goroutine 协程的通讯机制高并发

通道像一个传送带或者队列,老是遵循先入先出(First In First Out)的规则,保证收发数据的顺序,这一点和管道是同样的post

一个协程从通道的一头放入数据,另外一个协程从通道的另外一头读出数据性能

每个通道都是一个具体类型的导管,声明 channel 的时候须要为其指定元素类型。

通道能作什么?

控制协程的同步,让程序有序运行

GO 中提倡 不要经过共享内存来通讯,而经过通讯来共享内存

goroutine协程 是 Go 程序并发的执行体,channel 通道就是它们之间的链接,他们之间的桥梁,他们的交通枢纽

通道有哪几种?

大体可分为以下三种:

  • 无缓冲通道
  • 有缓冲的通道
  • 单向通道

无缓冲通道

无缓冲的通道又称为阻塞的通道

无缓冲通道上的发送操做会阻塞,直到另外一个goroutine在该通道上执行接收操做,这时值才能发送成功

两个 goroutine 协程将继续执行

咱们反过来看,若是接收操做先执行,接收方的goroutine将阻塞,直到另外一个 goroutine 协程在该通道上发送一个数据

所以,无缓冲通道也被称为同步通道,由于咱们可使用无缓冲通道进行通讯,利用发送和接收的 goroutine 协程同步化

有缓冲的通道

仍是上述提到的,有缓冲通道,就是在初始化 / 建立通道 的 make 函数的第 2 个参数填上咱们所指望的缓冲区大小 , 例如:

ch1 := make(chan int , 4)

此时,该通道的容量为4,发送方能够一直向通道中发送数据,直到通道满,且通道数据未被读走时,发送方就会阻塞

只要通道的容量大于零,那么该通道就是有缓冲的通道

通道的容量表示通道中能存放元素的数量

咱们可使用内置的 len函数 获取通道内元素的数量,使用 cap函数 获取通道的容量

单向通道

通道默认是既能够读有能够写的,可是单向通道就是要么只能读,要么只能写

  • chan <- int

是一个只能发送的通道,能够发送可是不能接收

  • <- chan int

是一个只能接收的通道,能够接收可是不能发送

如何建立和声明一个通道

声明通道

在 Go 里面,channel是一种类型,默认就是一种引用类型

简单解释一下什么是引用:

在咱们写C++的时候,用到引用会比较多

引用,顾名思义是某一个变量或对象的别名,对引用的操做与对其所绑定的变量或对象的操做彻底等价

在C++里面是这样用的:

类型 &引用名=目标变量名;

声明一个通道

var 变量名 chan 元素类型

var ch1 chan string               // 声明一个传递字符串数据的通道
var ch2 chan []int                 // 声明一个传递int切片数据的通道
var ch3 chan bool                  // 声明一个传递布尔型数据的通道
var ch4 chan interface{}          // 声明一个传递接口类型数据的通道

看,声明一个通道就是这么简单

对于通道来讲,关声明了还不能使用,声明的通道默认是其对应类型的零值,例如

  • int 类型 零值 就是 0
  • string 类型 零值就是个 空串
  • bool 类型 零值就是 false
  • 切片的 零值 就是 nil

咱们还须要对通道进行初始化才能够正常使用通道哦

初始化通道

通常是使用 make 函数初始化以后才能使用通道,也能够直接使用make函数 建立通道

例如:

ch5 := make(chan string)
ch6 := make(chan []int)
ch7 := make(chan bool)
ch8 := make(chan interface{})

make 函数的第二个参数是能够设置缓冲的大小的,咱们来看看源码的说明

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length. For example, make([]int, 0, 10) allocates an underlying array
// of size 10 and returns a slice of length 0 and capacity 10 that is
// backed by this underlying array.
// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.
func make(t Type, size ...IntegerType) Type

若是 make 函数的第二个参数不填,那么就默认是无缓冲的通道

如今咱们来看看如何操做 channel 通道,均可以怎么玩

如何操做 channel

通道的操做有以下三种操做:

  • 发送(send)
  • 接收(receive)
  • 关闭(close)

对于发送和接收通道里面的数据,写法就比较形象,使用 <- 来指向是从通道里面读取数据,仍是从通道中发送数据

向通道发送数据

// 建立一个通道
ch := make(chan int)
// 发送数据给通道
ch <- 1

咱们看到箭头的方向是,1 指向了 ch 通道,因此不难理解,这是将1 这个数据,放入通道中

从通道中接收数据

num := <-ch

不难看出,上述代码是 ch 指向了一个须要初始化的变量,也就是说,从 ch 中读出一个数据,赋值给 num

咱们从通道中读出数据,也能够不进行赋值,直接忽略也是能够的,如:

<-ch

关闭通道

Go中提供了 close 函数来关闭通道

close(ch)

对于关闭通道很是须要注意,用很差直接致使程序崩溃

  • 只有在通知接收方 goroutine 协程全部的数据都发送完毕的时候才须要关闭通道
  • 通道是能够被垃圾回收机制回收的,它和关闭文件是不同的,在结束操做以后关闭文件是必需要作的,但关闭通道不是必须的

关闭后的通道有如下 4 个特色:

  • 对一个关闭的通道再发送值就会致使 panic
  • 对一个关闭的通道进行接收会一直获取值直到通道为空
  • 对一个关闭的而且没有值的通道执行接收操做会获得对应类型的零值
  • 关闭一个已经关闭的通道会致使 panic

通道异常状况梳理

咱们来整理一下对于通道会存在的异常:

channel 状态 未初始化的通道(nil) 通道非空 通道是空的 通道满了 通道未满
接收数据 阻塞 接收数据 阻塞 接收数据 接收数据
发送数据 阻塞 发送数据 发送数据 阻塞 发送数据
关闭 panic 关闭通道成功
待数据读取完毕后
返回零值
关闭通道成功
直接返回零值
关闭通道成功
待数据读取完毕后
返回零值
关闭通道成功
待数据读取完毕后
返回零值

每一种通道的DEMO实战

无缓冲通道

func main() {
   // 建立一个无缓冲的,数据类型 为 int 类型的通道
   ch := make(chan int)
   // 向通道中写入 数字 1
   ch <- 1
   fmt.Println("send successfully ... ")
}

执行上述代码咱们能够查看到效果

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        F:/my_channel/main.go:9 +0x45
exit status 2

出现上述报错 deadlock 错误的缘由,细心的小伙伴应该可以知道为何,我上述有提到

咱们使用 ch := make(chan int) 建立的是无缓冲的通道

无缓冲的通道只有在有接收方接收值的时候才能发送数据成功

咱们能够想一下咱们生活中的案例同样:

你在某东上买了一个稍微贵重一点的物品,某东快递人员给你寄快递的时候,打电话给你,必需要送到你的手上,否则不敢签收,这个时候,你不方便,或者你不签收,那么这个快递就是算做没有寄送成功

所以,上述问题缘由是,建立了一个无缓冲通道,发送方一直在阻塞,通道中一直未有协程读取数据,致使死锁

咱们的解决办法就是建立另一个协程,将数据从通道中读出来便可

package main

import "fmt"

func recvData(c chan int) {
    ret := <-c
    fmt.Println("recvData successfully ... data = ", ret)
}

func main() {
    // 建立一个无缓冲的,数据类型 为 int 类型的通道
    ch := make(chan int)
    go recvData(ch)
    // 向通道中写入 数字 1
    ch <- 1
    fmt.Println("send successfully ... ")
}

这里须要注意,若是 go recvData(ch) 放在了 ch <- 1 以后,那么结果仍是同样的死锁,缘由仍是由于 ch <- 1 会一直阻塞,根本不会执行到 他以后的语句

实际效果

recvData successfully ... data =  1
send successfully ...

有缓冲通道

func main() {
   // 建立一个无缓冲的,数据类型 为 int 类型的通道
   ch := make(chan int , 1)
   // 向通道中写入 数字 1
   ch <- 1
   fmt.Println("send successfully ... ")
}

仍是一样的案例,一样的代码,咱们只是把无缓冲通道,换成了有缓冲的通道, 咱们仍然不专门开协程读取通道的数据

实际效果 , 发送成功

$$ $$

send successfully ...

由于此时通道中的缓冲是1,第一次向通道中发送数据,不会阻塞,

但是若是,在通道中数据还未读取出去以前,又向通道中写入数据,则此处会阻塞,

若一直没有协程从通道中读取数据,则结果与上述同样,会死锁

单向通道

package main

import "fmt"

func OnlyWriteData(out chan<- int) {
   // 单向 通道 , 只写 不能读
   for i := 0; i < 10; i++ {
      out <- i
   }
   close(out)
}

func CalData(out chan<- int, in <-chan int) {
   // out 单向 通道 , 只写 不能读
   // int 单向 通道 , 只读 不能写

   // 遍历 读取in 通道,若 in通道 数据读取完毕,则阻塞,若in 通道关闭,则退出循环
   for i := range in {
      out <- i + i
   }
   close(out)
}
func myPrinter(in <-chan int) {
   // 遍历 读取in 通道,若 in通道 数据读取完毕,则阻塞,若in 通道关闭,则退出循环
   for i := range in {
      fmt.Println(i)
   }
}

func main() {
   // 建立2 个无缓冲的通道
   ch1 := make(chan int)
   ch2 := make(chan int)


   go OnlyWriteData(ch1)
   go CalData(ch2, ch1)


   myPrinter(ch2)
}

咱们模拟 2 个通道,

  • 一个 只写 不能读
  • 一个 只读 不能写

实际效果

0
2
4
6
8
10
12
14
16
18

关闭通道

package main

import "fmt"

func main() {
   c := make(chan int)
   
   go func() {
      for i := 0; i < 10; i++ {
         // 循环向无缓冲的通道中写入数据, 只有当上一个数据被读走以后,下一个数据才能往通道中放
         c <- i
      }
      // 关闭通道
      close(c)
   }()
   for {
      // 读取通道中的数据,若通道中无数据,则阻塞,若读到 ok 为false, 则通道关闭,退出循环
      if data, ok := <-c; ok {
         fmt.Println(data)
      } else {
         break
      }
   }
   fmt.Println("channel over")
}

再次强调一下关闭通道,demo 的模拟方式与上述的案例基本一致,感兴趣的能够本身运行看看效果

看到这里,细心的小伙伴应该能够总结出,判断通道是否关闭的 2种 方式了吧?

  • 读取通道的时候,判断bool类型的变量是否为false

例如上述代码

if data, ok := <-c; ok {
    fmt.Println(data)
} else {
    break
}

判断 ok 为true,则正常读取到数据, 若为false ,则通道关闭

  • 经过 for range 的方式来遍历通道,若退出循环,则是由于通道关闭

sync 包

Go 的 sync 包也是用做实现并发任务的同步

还记得吗,在分享 文章GO的锁和原子操做分享的时候,咱们就用到过 sync 包

用法大同消息,这里列举一下 sync 包涉及的数据结构和方法

  • sync.WaitGroup
  • sync.Once
  • sync.Map

sync.WaitGroup

他是一个结构体,传递的时候要传递指针 ,这里须要注意

他是并发安全的,内部有维护一个计数器

涉及的方法:

  • (wg * WaitGroup) Add(delta int)

参数中 传入的 delta ,表示 sync.WaitGroup 内部的计数器 + delta

  • (wg *WaitGroup) Done()

表示当前协程退出,计数器 -1

  • (wg *WaitGroup) Wait()

等待并发任务执行完毕,此时的计数器为变成 0

sync.Once

他是并发安全的,内部有互斥锁 和 一个布尔类型的数据

  • 互斥锁 用于加锁解锁
  • 布尔类型的数据 用于记录初始化是否完成

通常用于在高并发的场景下只执行一次,咱们一会儿就能想到的场景会有程序启动时,加载配置文件的场景

针对相似的场景,Go 也给咱们提供了解决方法 ,即 sync.Once 里面的 Do 方法

  • func (o *Once) Do(f func()) {}

Do 方法的参数 是一个函数,但是咱们要在该函数里面传递参数咋整?

可使用Go 里面的闭包来实现 , 闭包的具体实现方式,感兴趣的能够深刻了解一下

sync.Map

他是并发安全的,正是由于 Go 中的 map 是并发不安全的,所以有了 sync.Map

sync.Map 有以下几个明显的优点:

  • 并发安全
  • sync.Map 不须要使用 make 初始化,直接使用 myMap := sync.Map{} 便可使用 sync.Map 里面的方法

sync.Map 涉及的方法

见名知意

  • Store

存入 key 和value

  • Load

取出 某个key 对应的 value

  • LoadOrStore

取出 而且 存入 2个操做

  • Delete

删除key 和 对应的 value

  • Range

遍历全部key 和 对应的 value

总结

  • 通道是什么,通道的种类
  • 无缓冲,有缓冲,单向通道具体对应什么
  • 对于通道的具体实践
  • 分享了关于通道的异常状况整理
  • 简单分享了sync包的使用

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提升质量的动力

好了,本次就到这里,下一次 服务注册与发现之 ETCD

技术是开放的,咱们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

相关文章
相关标签/搜索