Channel | 豆包MarsCode AI刷题

82 阅读6分钟

Channel

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

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

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

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

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

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

不要通过共享内存来通信,而应该通过通信来共享内存(chan)

 // 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
 }

死锁的其他情况:blog.csdn.net/liu19721018…

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
 }