乐趣区

关于后端:GO通道和-sync-包的分享

[TOC]

GO 通道和 sync 包的分享

咱们一起回顾一下上次分享的内容:

  • GO 协程同步若不做限度的话,会产生 数据竞态 的问题
  • 咱们用锁的形式来解决如上问题,依据应用场景抉择应用互斥锁 和 读写锁
  • 比应用锁更好的形式是原子操作,然而应用 go 的 sync/atomic须要小心应用,因为波及内存

要是对 GO 的锁和原子操作还感兴趣的话,欢送查看文章 GO 的锁和原子操作分享

上次咱们分享到锁和原子操作,都能够保障共享数据的读写

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

明天咱们来分享一下 Go 中举荐应用的其余同步办法,通道和 sync 包

通道是什么?

是一种非凡的类型,是连贯并发 goroutine 的管道

channel 通道是能够让一个 goroutine 协程发送特定值到另一个 goroutine 协程的 通信机制

通道 像一个传送带或者队列,总是遵循 先入先出 (First In First Out)的规定,保障收发数据的程序,这一点和 管道 是一样的

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

每一个通道都是一个具体类型的导管,申明 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

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

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

退出移动版