Go:channel

1. 向关闭的channel发送数据,会导致panic
2. v, ok <- ch; ok 为 bool 值,true 表示正常接收,false 表示通道关闭
3. 所有的 channel 接收者都会在 channel 关闭时,立刻从阻塞等待中返回且上述 ok 值为 false。
    这个广播机制常被利用,进行向多个订阅者同时发送信号。如:退出信号

一、channel

在 Go 语言里,不仅可以使用原子函数和互斥锁来保证对共享资源的安全访问以及消除竞争状态,还可以使用 channel,通过发送和接收需要共享的资源,在 goroutine 之间做同步。
当一个资源需要在 goroutine 之间共享时,channel 在 goroutine 之间架起了一通道,并提供了确保同步交换数据的机制。声明 channel时,需要指定将要被共享的数据的类型。可以通过 channel 共享内置类型、命名类型、结构类型和引用类型的值或者指针。

基本使用

package main

import (
    "fmt"
)

func main() {
    // 使用 make 创建 channel
    // 方式1
    var intChan chan int
    intChan = make(chan int, 3)
    // 方式2
    // intChan := make(chan int, 3)

    fmt.Printf("aChan的值:%v
", intChan)             // 0xc000086000
    fmt.Printf("aChan本身的地址:%p
", &intChan)     // 0xc000080018

    // 向管道发送值(注意给channel放入数据时,不能超过其容量)
    intChan <- 10
    intChan <- 20
    fmt.Printf("aChan的len:%v
", len(intChan)) // 2
    fmt.Printf("aChan的cap:%v
", cap(intChan)) // 3

    // 从管道中读取数据
    int1 := <- intChan
    int2 := <- intChan
    fmt.Println(int1, int2) // 10 20
}
示例1
package main

import (
    "fmt"
)

type Cat struct {
    Name string
    Age int
}

func main() {
    // 定义一个存放任意数据类型的管道3个数据
    allChan := make(chan interface{}, 3)
    allChan <- 10
    allChan <- "pd"
    cat := Cat{"tom", 10}
    allChan <- cat
    // 如果希望获得到管道中的第三个元素,则需先将前2个推出
    <- allChan
    <- allChan
    newCat := <- allChan
    fmt.Printf("newCat=%T , newCat=%v
", newCat, newCat)
    // fmt.Println(newCat.Name)     // 编译不通过,错误
    fmt.Println(newCat.(Cat).Name)    // 使用类型断言,正确
}
示例2

channel 使用注意事项:

  1. channel中只能存放指定的数据类型;
  2. channel的数据放满后,就不能在放入了;
  3. 如果从 channel 取出数据后,就可以继续放入了;
  4. 在没有使用 goroutine 的情况下,如果 channel 数据被取完了,再取,就会报 dead lock。

二、关闭 channel

关闭 channel 非常简单,直接使用Go语言内置的close()函数即可:
func main() {
	intChan := make(chan int, 3)
	intChan <- 10
	intChan <- 20
	// 关闭 channel
	close(intChan)
	// 关闭 channel 后,无法将数据写入到 channel 中,读取数据是可以的
	num := <- intChan
	fmt.Println(num) // 10
}

三、遍历 channel

channel 支持 for-range 的方式进行遍历:

  1. 在遍历时,如果 channel 没有关闭,则会出现 deadlock 错误;
  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    ch <- 30
    // 关闭 channel
    close(ch)
    // 遍历 channel
    for v := range ch {
        fmt.Println(v)
    }
}
View Code

四、应用实例

实例1:

  1. 开启一个 writeData 协程,向管道中写入30个整数;
  2. 开启一个 readData 协程,从管道中读取writeData写入的数据;
  3. writeData 和 readData 操作的是同一个管道;
  4. 主线程需要等待这两个协程都完成工作才能退出。
package main

import "fmt"

// 将数据放入管道
func writeData(intChan chan int) {
    for i := 1; i <= 30; i++ {
        // 放入数据
        intChan<- i
        fmt.Printf("写入数据:%v
", i)
    }
    close(intChan) // 关闭管道
}

// 从管道中获取数据
func readData(intChan chan int, exitFlagChan chan bool) {
    for {
        v, ok := <-intChan
        if !ok {
            break
        }
        fmt.Printf("获取数据:%v
", v)
    }
    // 读取完数据后,写入一个"标志"到另一个管道
    exitFlagChan<- true
    close(exitFlagChan) // 关闭管道
}

func main() {
    intChan := make(chan int, 10)
    exitFlagChan := make(chan bool, 1)
    go writeData(intChan)
    go readData(intChan, exitFlagChan)
    // 获取"标志",用于结束主线程
    for {
        _, ok := <-exitFlagChan
        if !ok {
            break
        }
    }
}
View Code

阻塞:

对于示例1中,如果注销掉上图中的这行代码(即相当于只是向管道写入数据,而没有获取数据)。就会出现阻塞而发生 deadlock,原因是 intChan 的容量是10,而写入数据确是 50 个。

如果,编译器运行时发现一个管道只有写,没有读,则该管道会阻塞。PS:写管道和读管道的速度(频率)不一致,是没有关系的。

实例2:

需求:统计 1-5000 的数字中,那些是素数?

分析:

  • 传统的方式:使用一个循环,判断各个数是不是素数;
  • 协程的方式:将统计的素数的任务分配给(4个)goroutine 去完成(完成任务时间短)。
package main

import "fmt"

// 向intChan放入1-5000个整数
func putNum(intChan chan int) {
    for i := 1; i <= 5000; i++ {
        intChan<- i
    }
    close(intChan)
}

// 从intChan取出数据,并判断是否为素数,如果是,就放入到resultChan中
func getNum(intChan chan int, resultChan chan int, exitChan chan bool) {
    var flag bool
    for {
        num, ok := <-intChan
        if !ok {
            // intChan 取不到
            break
        }
        // flag默认为true(假设取到的是素数)
        flag = true
        // 判断num是不是素数
        for i := 2; i < num; i++ {
            if num % i == 0 { // 说明该num不是素数
                flag = false
                break
            }
        }
        if flag {
            // 将这个数就放入到resultChan
            resultChan<- num
        }
    }
    // 还不能关闭 resultChan,因为是4个协程在工作
    // 向 exitChan 写入true
    exitChan<- true
}

func main() {
    // 保存原始数据的管道
    intChan := make(chan int , 500)
    // 保存结果的管道
    resultChan := make(chan int, 1000)
    // 标识退出的管道
    exitChan := make(chan bool, 4) // 4个
    // 产生数据
    go putNum(intChan)
    // 开启4个协程,从intChan取出数据,并判断是否为素数,如果是,就放入到resultChan
    for i := 0; i < 4; i++ {
        go getNum(intChan, resultChan, exitChan)
    }
    // 主线程进行处理,当从resultChan取出了4个结果,就可以放心的关闭resultChan
    go func(){
        for i := 0; i < 4; i++ {
            <-exitChan
        }
        close(resultChan)
    }()
    // 遍历resultChan,把结果取出
    count := 0
    for {
        r, ok := <-resultChan
        if !ok{
            break
        }
        count += 1
        fmt.Printf("素数:%d
", r)
    }
    fmt.Printf("素数数量:%d
", count)
    fmt.Println("主线程退出")
}
View Code

  

原文地址:https://www.cnblogs.com/believepd/p/10957377.html