Golang-Channel讲解

Channel讲解

上节回顾

1、程序、进程、线程、协程

2、go

3、runtime

4、临界资源安全问题(单线程下不会出现的问题,在多线程下会出现)

5、锁 sync

  • mutex

  • waitgroup

Channel

1、Go语言不建议我们使用锁机制来解决多线程问题、建议我们使用通道。

2、通信的角色,必须在2个以上。1个人,不能叫做通信。

3、chan ,必须要作用在两个及两个以上的 goroutine .

4、一个goroutine需要将一些信息告诉另外一个goroutine ,就直接将数据信息放入chan即可。

通道:可以被认为是 Gr 通信管道。

类似于水管,数据可以从一端流到另一端。

不要通过共享内存来通信,而应该通过通信来共享内存(chan)” 这是一句风靡golang社区的经典语

// chan 类型 ,通道
var a chan int
a = make(chan int)
​
// 使用规则(存 chan<-、取 <-chan)
a <- 1
data := <- a 
package main
​
import (
   "fmt"
   "time"
)
​
// 定义通道 chan
// 这个 goroutine 希望告诉 main 线程,我还没结束。(通信)
func main() {
   // 定一个bool的通道
   var ch chan bool
   ch = make(chan bool)
​
   // 在一个goroutine中去往通道中放入数据
   go func() {
      for i := 0; i < 10; i++ {
         fmt.Println("goroutine-", i)
      }
      time.Sleep(time.Second * 3)
      ch <- true
   }()
​
   // 另一个goroutine可以从通道中取出数据。(线程之间的通信)
   // 阻塞等待ch拿到值。有另外一个goroutine往里放值。
   data := <-ch
   fmt.Println("ch data:", data)
}

一个通道发送和接收数据,默认是阻塞的。

当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。

相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。

本身channel就是同步的, 意味着同一时间,只能有一条goroutine来操作。

最后:通道是goroutine之间的连接,所有通道的发送和接收必须处在不同的goroutine中

这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量

死锁

如果创建了chan,没有 Goroutine 来使用了,则会出现死锁。

使用通道时要考虑的一一个重要因素是死锁。如果Goroutine在一 个通道 上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。

类似地,如果Goroutine 正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。

package main
​
import (
   "fmt"
)
​
// 定义通道 chan
// 这个 goroutine 希望告诉 main 线程,我还没结束。(通信)
func main() {
   // 定一个bool的通道
   var ch chan bool
   ch = make(chan bool)
​
    在一个goroutine中去往通道中放入数据
   go func() {
      for i := 0; i < 10; i++ {
         fmt.Println("goroutine-", i)
      }
      //time.Sleep(time.Second * 3)
      ch <- true
   }()
​
   // 定义好通道之后,如果没有 goroutine来使用(必须在两个及以上goroutine),那么就会产生死锁
   // deadlock!
   data := <-ch
   fmt.Println("ch data:", data)
​
   // 死锁的产生,没有goroutine来消耗通道(存取)
   ch2 := make(chan int)
   ch2 <- 10
}

死锁的其他情况:Go语言之死锁的4种常见情况_go 死锁-CSDN博客

1、单线的使用,没有其他的goroutine消费

2、两个chan,互相需要对方的数据,但是由于判断,拿不到对方的数据。

3、sync 锁产生的死锁。

关闭通道

package main
​
import (
   "fmt"
   "time"
)
​
// 关闭通道
// 告诉接收方,我不会再有其他数据发送到chan了。
func main() {
   // 在main线程中定义的通道
   ch1 := make(chan int)
   go test7(ch1)
   // 读取chan中的数据
   for {
      time.Sleep(time.Second)
      // ok 判断chan的状态是否是关闭,如果是关闭,不会再取值了。
      // ok, 如果是true,就代表我们还在读数据
      // ok, 如果是fasle,就说明该通道已关闭
      data, ok := <-ch1
      if !ok {
         fmt.Println("读取完毕", ok)
         break
      }
      fmt.Println("ch1 data:", data)
   }
}
​
// 通道可以参数传递
func test7(ch chan int) {
   for i := 0; i < 10; i++ {
      ch <- i
   }
   // 关闭通道,告诉接收方,不会在往ch中放入数据
   close(ch)
}

通过ok来判断是否读取完毕数据。

for range 可以简化开发

package main
​
import (
   "fmt"
   "time"
)
​
// 关闭通道
// 告诉接收方,我不会再有其他数据发送到chan了。
func main() {
   // 在main线程中定义的通道
   ch1 := make(chan int)
   go test7(ch1)
   // 读取chan中的数据, for 一个个取,并且会自动判断chan是否close 迭代器
   for data := range ch1 {
      time.Sleep(time.Second)
      fmt.Println(data)
   }
   fmt.Println("end")
}
​
// 通道可以参数传递
func test7(ch chan int) {
   for i := 0; i < 10; i++ {
      ch <- i
   }
   // 关闭通道,告诉接收方,不会在往ch中放入数据
   close(ch)
}

缓冲通道(chan)

非缓冲通道

chan , 只能存放一个数据,发送和接受都是阻塞的。一次发送对应一个接收。

缓冲通道

通道带了一个缓冲区,发送的数据直到缓冲区填满为止,才会被阻塞,接收的也是,只有缓冲区清空,才会阻塞。

chan如果只有一个容量,老是阻塞,效率是很低的。

package main
​
import (
   "fmt"
   "strconv"
   "time"
)
​
// 缓冲通道 chan,cap
func main() {
​
   // 非缓冲通道
   ch1 := make(chan int)
   fmt.Println(cap(ch1), len(ch1)) // 0 0
   //ch1 <- 100
​
   // 缓冲通道
   // 缓冲区通道,放入数据,不会产生死锁,它不需要等待另外的线程来拿,它可以放多个数据。
   // 如果缓冲区满了,还没有人取,也会产生死锁。
   ch2 := make(chan string, 5)
   fmt.Println(cap(ch2), len(ch2)) // 5 0
   ch2 <- "1"
   fmt.Println(cap(ch2), len(ch2)) // 5 1 , 可以通过len来判断缓冲通道中的数据数量
   ch2 <- "2"
   ch2 <- "3"
   fmt.Println(cap(ch2), len(ch2)) // 5 3
   ch2 <- "4"
   ch2 <- "5"
   fmt.Println(cap(ch2), len(ch2)) // 5 5
   data := <-ch2
   ch2 <- "6" // deadlock!
   fmt.Println(data)
​
   ch3 := make(chan string, 4)
   go test8(ch3)
   fmt.Println("--------------------------")
   for s := range ch3 {
      time.Sleep(time.Second)
      fmt.Println("main中读取的数据:", s)
   }
   fmt.Println("main-end")
}
​
func test8(ch chan string) {
   for i := 0; i < 10; i++ {
      ch <- "test - " + strconv.Itoa(i)
      fmt.Println("子goroutine放入数据:", "test - "+strconv.Itoa(i))
   }
   close(ch)
}

缓冲通道,可以定义缓冲区的数量

如果缓冲区没有满,可以继续存放,如果满了,也会阻塞等待

如果缓冲区空的,读取也会等待,如果缓冲区中有多个数据,依次按照先进先出的规则进行读取。

如果缓冲区满了,同时有两个线程在读或者写,这个时候和普通的chan一样。一进一出。

定向通道

双向通道

channel 是用来实现 goroutine 通信的。一个写、一个读、这是双向通道。

ch <- data
data := <- ch

单向通道 只能读或者只能写

package main
​
import (
   "fmt"
   "time"
)
​
// 单向通道使用场景
func main() {
​
   ch1 := make(chan int) // 可读可写
   go writeOnly(ch1)
   go readOnly(ch1)
​
   time.Sleep(time.Second * 3)
}
​
// 作为函数的参数或者返回值之类的。
// 指定函数去写,不让他读取,防止通道滥用
func writeOnly(ch chan<- int) {
   // 函数的内部,处理一些写数据的操作
   ch <- 100
}
​
// 指定函数去读,不让他写,防止通道滥用
func readOnly(ch <-chan int) int {
   // 取出通道的值,做一些操作,不可写的。
   data := <-ch
   fmt.Println(data)
   return data
}

Select

select 只能在通道中使用。

package main
​
import (
   "fmt"
   "time"
)
​
// select
func main() {
​
   ch1 := make(chan int)
   ch2 := make(chan int)
​
   go func() {
      time.Sleep(time.Second * 2)
      ch1 <- 100
   }()
​
   go func() {
      time.Sleep(time.Second * 2)
      ch2 <- 200
   }()
​
   // 读取chan数据,无论谁先放入,我们就用谁,抛弃其他的.
   // select 和 swtich 只是在通道中使用,case表达式需要是一个通道结果
   select {
   case num1 := <-ch1:
      fmt.Println(num1)
   case num2 := <-ch2:
      fmt.Println(num2)
      //default:
      // fmt.Println("default")
   }
​
}

1、每一个case必须是一个通道的操作 <-

2、所有chan操作都有要结果(通道表达式都必须会被求值)

3、如果任意的通道拿到了结果。它就会立即执行该case、其他就会被忽略

4、如果有多个case都可以运行,select是随机选取一个执行,其他的就不会执行。

5、如果存在default,执行该语句,如果不存在,阻塞等待 select 直到某个通道可以运行。

Timer 定时器

通道的应用场景一。

可以控制程序在某个事件做某件事情。

package main
​
import (
   "fmt"
   "time"
)
​
// 定时器(time : 当下,xxx之前before,xxxx之后 after)
func main() {
   // 创建一个定时器 NewTimer
   // Timer  C <-chan Time
   //timer := time.NewTimer(time.Second * 3)
   // 当前时间
   //fmt.Println(time.Now())
   // timer.C, 时间通道,这个通道中存放的值 2023-03-02 21:49:48.0453619 +0800 CST m=+3.013284501
   // 就是我们在定义定时器的时候,存放的时间,等待对应的时间。
   //timeChan := timer.C
   //fmt.Println(<-timeChan)
​
   // 定时器(提前关闭)
   // 会向Timer.C 放入一个时间。
   timer2 := time.NewTimer(time.Second * 5)
   go func() {
      <-timer2.C // 消费
      fmt.Println("end")
   }()
   timer2.Stop() // 手动停止定时器。
}

1、定时发邮件

2、定时保存数据库文件 sql

After Before

package main
​
import (
   "fmt"
   "time"
)
​
func main() {
   // chan 放入当前时间之后的某个时间
   //timerChan := time.After(time.Second * 3)
   //fmt.Println(time.Now())
   //chanTime := <-timerChan
   //fmt.Println(chanTime)
​
   // 在3s以后执行这个函数
   time.AfterFunc(time.Second*3, mail)
​
   time.Sleep(5 * time.Second)
}
func mail() {
   // 发邮件
   fmt.Println("发邮件")
}

作业:

1、通过chan来解决售票问题

2、了解chan的一些其他应用在Go的源码中

3、gouroutine的使用,必须要熟练,掌握死锁的情况。

4、了解GPM模型(Golang关于gouroutine的一个底层模型)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值