Fork me on GitHub

Goroutine之Channel

一、channel基础

1、引入

在Goroutine基础中我们通过WaitGroup解决了主线程因为无法感知其它协程是否结束而造成提前结束的问题,通过锁机制解决了多协程之间共享数据而造成数据混乱和安全的问题。归结起来协程问题:

  • 资源竞争,数据共享而引发数据安全问题
  • 一个协程不知道另一个协程什么时候结束

那么通过之前的方式解决这些问题,还可以通过更高级一点的手段,那就是channel管道。

channel的本质是一个队列(先进先出)的数据结构,多goroutine访问时不需要加锁,本身就是安全的,channel中的元素时有类型的,int类型的只能放int类型。

 2、语法

var 变量名 chan 数据类型

例如:

var intChan chan int // intChan中存放的是int类型
var mapChan chan map[int]string// mapChan 中存放的是map[int]string类型
var userChan chan User//Person结构体类型
var userChan chan *User//Person指针类型
...
 

注意:

  • channel是引用类型,所以传入函数中的channel是同一个channel
  • channel必须初始化,即make后使用
  • channel是有类型的
package main

import "fmt"

func main() {
    // 创建一个可以存放5个int类型的管道
    var intChan chan int = make(chan int, 5)

    // intChan是一个引用类型
    fmt.Printf("intChan的值=%v, intChan本身的地址=%p \n", intChan, &intChan) // intChan的值=0xc000020090, intChan本身的地址=0xc000006028

    // 向管道中写入数据,写入的数据不能超过容量
    intChan <- 20
    intChan <- 10

    // 查看管道的长度和容量(cap)
    fmt.Printf("len=%v, cap=%v \n", len(intChan), cap(intChan)) // len=2, cap=5

    // 读取管道中的值
    var num1 int
    num1 = <-intChan
    fmt.Println(num1) // 20

    //再次读取长度和容量
    fmt.Printf("len=%v, cap=%v \n", len(intChan), cap(intChan)) // len=1, cap=5
}

可以看到管道中一边写入数据,一边取出数据,但是如果管道中的数据取完了,还继续取出就会出现deadlock思索问题。 

3、channel的遍历与关闭

使用内置函数close可以关闭channel,当channel关闭后就不能向channel写数据了,但是仍然可以从该channel中读取数据。 

 channel支持for-range的方式遍历,注意的是:

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

import "fmt"

func main() {

    var intChan chan int = make(chan int, 1000)

    for i := 0; i < 800; i++ {

        intChan <- i // 管道中放入800个数

    }
    // 放入数字后,关闭管道,循环结束不会报deadlock问题
    close(intChan)

    // 遍历管道使用for-range, 不能使用普通循环
    for v := range intChan {
        fmt.Println("v=", v)
    }

}

二、管道阻塞

channel的用法一般时既有写又有读,类似于生产者、消费者模型,但是如果只有生产者,而没有消费者就会出现阻塞。比如,管道的容量是10,但是你如果向里面放入30个数就会出现阻塞,从而导致deadlock的产生。比如:

package main

func main() {

    var numChan chan int = make(chan int, 10)

    for i := 0; i < 30; i++ {

        numChan <- i

    }

}
/*
fatal error: all goroutines are asleep - deadlock!
*/

那么如何解决这个问题呢?那么就需要避免阻塞,读写频率可以不同但是必须两方都存在。如:

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
var numChan chan int = make(chan int, 1000)

func write(numChan chan int) {
    defer wg.Done()
    for i := 0; i < 500; i++ {
        numChan <- i
    }
    close(numChan) //写入完毕后一定要关闭管道,否则读取到最后会deadlock

}

func read(numChan chan int) {
    defer wg.Done()

    for {
        v, flag := <-numChan
        if !flag {
            break
        }
        fmt.Println(v)
        time.Sleep(time.Second)
    }

}

func main() {
    wg.Add(2)
    // 启动2个协程
    go write(numChan)
    go read(numChan)

    wg.Wait()

}

可以看到上面的写的协程速度很快,但是读的很慢,但是golang的机制会检测出有人消费就不会出现deadlock。

三、select

  select语句可以选择一组可能的send操作或者receive操作,类似switch,但是只是用来处理通讯操作。case后的语句可以是send语句,也可以是receive语句,亦或者是default语句。最多同时允许一个case语句执行,如果所有的case都没有匹配上就会执行default语句。那么使用它可以处理什么问题呢?解决未关闭管道channel造成阻塞而出现deadlock问题。

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
var numChan chan int = make(chan int, 1000)

func write(numChan chan int) {
    defer wg.Done()
    for i := 0; i < 10; i++ {
        numChan <- i
    }
    //close(numChan) //下面通过select解决了如果不关闭管道出现阻塞而产生deadlock问题

}

func read(numChan chan int) {
    defer wg.Done()

    for {
        // 有时并不清楚何时关闭管道,通过select解决未关闭管道出现deadlock问题
        select {
        case v := <-numChan:
            fmt.Println(v)
            time.Sleep(time.Second)

        default:
            fmt.Println("读取结束")
            return

        }

    }

}

func main() {
    wg.Add(2)
    // 启动2个协程
    go write(numChan)
    go read(numChan)

    wg.Wait()

}

 有时我们并不清楚何时关闭管道,所以通过select方式可以在写完数据不关闭管道避免出现deadlock问题。

四、单向管道

 管道在默认情况下是双向的,但是它可以声明为只读或者只写:

// 只读声明
var numChan <-chan int

// 只写声明
var numChan chan<-  int

例如:

package main

import "fmt"

func main() {

    var numChan chan<- int // 声明一个只写管道

    numChan <- 1

    var numChan1 <-chan int // 声明一个只读管道
    var num1 = <-numChan1
    fmt.Println(num1)

}

应用场景:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func send(numChan chan<- int) {
    // chan<- 只写
    defer wg.Done()
    for i := 0; i < 20; i++ {
        numChan <- i
    }
    close(numChan)

}

func recv(numChan <-chan int) {
    // <-chan 只读
    defer wg.Done()
    for {
        v, flag := <-numChan
        if !flag {
            break
        }
        fmt.Println(v)
    }
}

func main() {

    var numChan chan int = make(chan int, 20)
    wg.Add(2)

    go send(numChan)
    go recv(numChan)

    wg.Wait()

}

五、实战演练

完成goroutine和channel的协同工作:

  • 开启一个writeData协程,向管道intChan写入50个数
  • 开启一个readData协程,从管道intChan中读出数据

注意:上述两个协程操作的是同一个管道,主协程需要等上述两个协程都完成工作才能退出

思路分析:

 代码实现:

package main

import "fmt"

func writeData(intChan chan int) {
    for i := 0; i < 50; i++ {
        intChan <- i
    }
    defer close(intChan)
}

func readData(intChan chan int, exitChan chan bool) {
    for {
        v, flag := <-intChan
        if !flag {
            break
        }
        fmt.Println(v)
    }
    // 完成读取协程,向exitChan发送信息
    exitChan <- true
    // 关闭管道
    defer close(exitChan)
}

func main() {
    var intChan chan int = make(chan int, 50)
    var exitChan chan bool = make(chan bool, 1)

    go writeData(intChan)
    go readData(intChan, exitChan)

    for {
        _, flag := <-exitChan
        if flag {
            // 说明读取协程结束,可以结束主协程了
            break
        }
    }

}

 

posted @ 2022-02-01 17:27  iveBoy  阅读(81)  评论(0)    收藏  举报
TOP