GO语言使用之channel(管道)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/TDCQZD/article/details/82684750

一、为什么需要channel

1、需求:
现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成

1)、分析思路:
使用goroutine 来完成,效率高,但是会出现并发/并行安全问题.
这里就提出了不同goroutine如何通信的问题

2)、代码实现
使用goroutine来完成(看看使用gorotine并发完成会出现什么问题? 然后我们会去解决)
在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增加一个参数 -race即可
3)、示意图
这里写图片描述
4)、代码实现:

package utils


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

// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成 

// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.

var (
    myMap = make(map[int]int, 10)
)

// cacluFactorial 函数就是计算 n!, 让将这个结果放入到 myMap
func cacluFactorial(n int) {

    res := 1
    for i := 1; i <= n; i++ {
        res *= i
    }




    //这里我们将 res 放入到myMap
    myMap[n] = res //concurrent map writes?



}

func FactorialDemo() {

    // 我们这里开启多个协程完成这个任务[200个]
    for i := 1; i <= 200; i++ {
        go cacluFactorial(i)
    }


    //休眠10秒钟【第二个问题 】
    time.Sleep(time.Second * 10)



    //这里我们输出结果,变量这个结果
    for i, v := range myMap {
        fmt.Printf("map[%d]=%d\n", i, v)
    }



}

2、不同goroutine之间如何通讯

  • 全局变量的互斥锁
  • 使用管道channel来解决

3、使用全局变量加锁同步改进程序
因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes
解决方案:加入互斥锁
我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)
代码改进

package utils


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

// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成 

// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.

var (
    myMap = make(map[int]int, 10)

    /*同步锁改进代码*/
    //声明一个全局互斥锁
    lock sync.Mutex //sync包提供了基本的同步基元,如互斥锁。Mutex是一个互斥锁, 
)

// cacluFactorial 函数就是计算 n!, 让将这个结果放入到 myMap
func cacluFactorial(n int) {

    res := 1
    for i := 1; i <= n; i++ {
        res *= i
    }


    /*同步锁改进代码*/
    lock.Lock()//加锁

    //这里我们将 res 放入到myMap
    myMap[n] = res //concurrent map writes?

    lock.Unlock()//解锁

}

func FactorialDemo() {

    // 我们这里开启多个协程完成这个任务[200个]
    for i := 1; i <= 200; i++ {
        go cacluFactorial(i)
    }


    //休眠10秒钟【第二个问题 】
    time.Sleep(time.Second * 10)

    /*同步锁改进代码*/
    lock.Lock()//加锁

    //这里我们输出结果,变量这个结果
    for i, v := range myMap {
        fmt.Printf("map[%d]=%d\n", i, v)
    }

    lock.Unlock()//解锁

}

4、为什么需要channel

前面使用全局变量加锁同步来解决goroutine的通讯,但不完美。

主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,仅仅是估算。

如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁。

通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
上面种种分析都在呼唤一个新的通讯机制-channel

二、channel的基本介绍

1、channle本质就是一个数据结构-队列
数据是先进先出【FIFO : first in first out】
线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
channel有类型的,一个string的channel只能存放string类型数据。
示意图:
这里写图片描述
2、定义/声明channel
语法
var 变量名 chan 数据类型
举例

var   intChan   chan  int (intChan用于存放int数据)
var   mapChan chan map[int]string (mapChan用于存放map[int]string类型)
var   perChan  chan  Person 
var   perChan2  chan  *Person 


说明
channel是引用类型
channel必须初始化才能写入数据, 即make后才能使用
管道是有类型的,intChan 只能写入 整数 int

三、快速入门案例

package utils
import (
    "fmt"
)

//管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
func main() {

    //演示一下管道的使用
    //1. 创建一个可以存放3个int类型的管道
    var intChan chan int
    intChan = make(chan int, 3)

    //2. 看看intChan是什么
    fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)


    //3. 向管道写入数据
    intChan<- 10
    num := 211
    intChan<- num
    intChan<- 50
    // intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量


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

    //5. 从管道中读取数据

    var num2 int
    num2 = <-intChan 
    fmt.Println("num2=", num2)
    fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))  // 2, 3

    //6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock

    num3 := <-intChan
    num4 := <-intChan
    num5 := <-intChan

    fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)

}

测试结果:

channel len= 3 cap=3
num2= 10
channel len= 2 cap=3
fatal error: all goroutines are asleep - deadlock!

总结:channel使用的注意事项

channel中只能存放指定的数据类型
channle的数据放满后,就不能再放入了
如果从channel取出数据后,可以继续放入
在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock

四、channel的遍历和关闭

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

2、channel的遍历
channel支持for–range的方式进行遍历,请注意两个细节
在遍历时,如果channel没有关闭,则回出现deadlock的错误
在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
3、案列演示

package utils

import (
    "fmt"
)

func TranslateDemo()  {
    intChan := make(chan int, 3)
    intChan<- 100
    intChan<- 200
    close(intChan) // close
    //这是不能够再写入数到channel
    //intChan<- 300
    fmt.Println("okook~")
    //当管道关闭后,读取数据是可以的
    n1 := <-intChan
    fmt.Println("n1=", n1)


    //遍历管道
    intChan2 := make(chan int, 100)
    for i := 0; i < 100; i++ {
        intChan2<- i * 2  //放入100个数据到管道
    }

    //遍历管道不能使用普通的 for 循环
    // for i := 0; i < len(intChan2); i++ {

    // }
    //在遍历时,如果channel没有关闭,则会出现deadlock的错误
    //在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
    close(intChan2)
    for v := range intChan2 {
        fmt.Println("v=", v)
    }

}

五、 goroutine和channel结合案例

package utils

import (
    "fmt"
)

/*goroutine和channel结合
请完成goroutine和channel协同工作的案例,具体要求:
开启一个writeData协程,向管道intChan中写入50个整数.
开启一个readData协程,从管道intChan中读取writeData写入的数据。
注意: writeData和readDate操作的是同一个管道
主线程需要等待writeData和readDate协程都完成工作才能退出【管道】


*/
func writeData(intChan chan int) {
    for i := 1; i <= 50; i++ {
        //放入数据
        intChan<- i
        fmt.Println("writeData ", i)
        //time.Sleep(time.Second)
    }
    close(intChan) //关闭
}

//read data
func readData(intChan chan int, exitChan chan bool) {

    for {
        v, ok := <-intChan
        if !ok {
            break
        }
        //time.Sleep(time.Second)
        fmt.Printf("readData 读到数据=%v\n", v) 
    }
    //readData 读取完数据后,即任务完成
    exitChan<- true
    close(exitChan)

}

func Test() {

    //创建两个管道
    intChan := make(chan int, 50)
    exitChan := make(chan bool, 1)

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

    //time.Sleep(time.Second * 10)
    for {
        _, ok := <-exitChan
        if !ok {
            break
        }
    }

}

六、 channel使用细节和注意事项

1、channel可以声明为只读,或者只写性质 【案例演示】
这里写图片描述
2、channel只读和只写的最佳实践案例
这里写图片描述
3、使用select可以解决从管道取数据的阻塞问题【案例演示】

package utils

// 使用select可以解决从管道取数据的阻塞问题

import (
    "fmt"
    "time"
)

func SelectDemo() {

    //使用select可以解决从管道取数据的阻塞问题

    //1.定义一个管道 10个数据int
    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
        intChan<- i
    }
    //2.定义一个管道 5个数据string
    stringChan := make(chan string, 5)
    for i := 0; i < 5; i++ {
        stringChan <- "hello" + fmt.Sprintf("%d", i)
    }

    //传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock

    //问题,在实际开发中,可能我们不好确定什么关闭该管道.
    //可以使用select 方式可以解决
    //label:
    for {
        select {
            //注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
            //,会自动到下一个case匹配
            case v := <-intChan : 
                fmt.Printf("从intChan读取的数据%d\n", v)
                time.Sleep(time.Second)
            case v := <-stringChan :
                fmt.Printf("从stringChan读取的数据%s\n", v)
                time.Sleep(time.Second)
            default :
                fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
                time.Sleep(time.Second)
                return 
                //break label
        }
    }
}

4、goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题.【案例演示】

说明: 如果我们起了一个协程,但是这个协程出现了panic, 如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic, 进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续执行。

package utils

import (
    "fmt"
    "time"
)

//函数
func sayHello() {
    for i := 0; i < 10; i++ {
        time.Sleep(time.Second)
        fmt.Println("hello,world")
    }
}
//函数
func testRecover() {
    //这里我们可以使用defer + recover
    defer func() {
        //捕获test抛出的panic
        if err := recover(); err != nil {
            fmt.Println("test() 发生错误", err)
        }
    }()
    //定义了一个map
    var myMap map[int]string
    myMap[0] = "golang" //error
}

func RecoverDemo() {

    go sayHello()
    go testRecover()


    for i := 0; i < 10; i++ {
        fmt.Println("main() ok=", i)
        time.Sleep(time.Second)
    }

}

猜你喜欢

转载自blog.csdn.net/TDCQZD/article/details/82684750