• Go中的channel


    channel

    Go语言中的通道(channel)是一种特殊的类型。
    在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。

    通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

    (1)channel本身是一个队列,先进先出
    (2)线程安全,不需要加锁
    (3)本身是有类型的,string, int 等,如果要存多种类型,则定义成 interface类型
    (4)channel是引用类型,必须make之后才能使用,一旦 make,它的容量就确定了,不会动态增加!!它和map,slice不一样

    特点:
    (1)一旦初始化容量,就不会改变了。
    (2)当写满时,不可以写,取空时,不可以取。
    (3)发送将持续阻塞直到数据被接收
    把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go 程序运行时能智能地发现一些永远无法发送成功的语句并做出提示
    (4)接收将持续阻塞直到发送方发送数据。
    如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
    (5)每次接收一个元素。
    通道一次只能接收一个数据元素。

    1、关于 channel的声明和使用的代码:

    package main
    import (
    	"fmt"
    )
    
    func main() {
    
    	//演示一下管道的使用
    	//1. 创建一个可以存放3个int类型的管道
    	var intChan chan int
    	intChan = make(chan int, 3)
    
    	//2. 看看intChan是什么
    	fmt.Printf("intChan 的值=%v intChan本身的地址=%p
    ", intChan, &intChan)
    
    
    	//3. 向管道写入数据
    	intChan<- 10
    	num := 211
    	intChan<- num
    	intChan<- 50
    	// //如果从channel取出数据后,可以继续放入
    	<-intChan
    	intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量
    
    
    	//4. 看看管道的长度和cap(容量)
    	fmt.Printf("channel len= %v cap=%v 
    ", len(intChan), cap(intChan)) // 3, 3
    
    	//5. 从管道中读取数据
    
    	var num2 int
    	num2 = <-intChan 
    	fmt.Println("num2=", num2)
    	fmt.Printf("channel len= %v cap=%v 
    ", len(intChan), cap(intChan))  // 2, 3
    
    	//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
    
    	num3 := <-intChan
    	num4 := <-intChan
    
    	//num5 := <-intChan
    
    	fmt.Println("num3=", num3, "num4=", num4/*, "num5=", num5*/)
    
    }
    
    
    fmt.Printf("intChan 的值=%v intChan本身的地址=%p
    ", intChan, &intChan)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    这句代码显示:channel其实和指针一样,本身存放在一个内存单元中,有它的地址,而它的值是一个 int类型的地址。

    2、注意空接口类型的 channel

    package main
    import (
    	"fmt"
    )
    
    type Cat struct {
    	Name string
    	Age int
    }
    
    func main() {
    
    	//定义一个存放任意数据类型的管道 3个数据
    	//var allChan chan interface{}
    	allChan := make(chan interface{}, 3)
    
    	allChan<- 10
    	allChan<- "tom jack"
    	cat := Cat{"小花猫", 4}
    	allChan<- cat
    
    	//我们希望获得到管道中的第三个元素,则先将前2个推出
    	<-allChan
    	<-allChan
    
    	newCat := <-allChan //从管道中取出的Cat是什么?
    
    	fmt.Printf("newCat=%T , newCat=%v
    ", newCat, newCat)
    	//下面的写法是错误的!编译不通过
    	//fmt.Printf("newCat.Name=%v", newCat.Name)
    	//使用类型断言
    	a := newCat.(Cat) 
    	fmt.Printf("newCat.Name=%v", a.Name)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    定义 interface类型的空接口,可以接收任意类型的数据,但是在取出来的时候,必须断言!
    a := newCat.(Cat)

    3、channel的关闭:close( )

    关闭之后,不能再写入,只能读。
    只能由发送者执行这句代码

    4、channel的遍历: for … range

    通道的数据接收一共有以下 4 种写法。

    1. 阻塞接收数据
      阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:

      data := <-ch

    执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。

    1. 非阻塞接收数据(有问题啊,还是会报错deadlock
      使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:

      data, ok := <-ch

    data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
    ok:表示是否接收到数据。

    非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel进行,可以参见后面的内容。

    1. 接收任意数据,忽略接收的数据
      阻塞接收数据后,忽略从通道返回的数据,格式如下:

      <-ch

    执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。

    使用通道做并发同步的写法,可以参考下面的例子:

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        // 构建一个通道
        ch := make(chan int)
    
        // 开启一个并发匿名函数
        go func() {
    
            fmt.Println("start goroutine")
    
            // 通过通道通知main的goroutine
            ch <- 0
    
            fmt.Println("exit goroutine")
    
        }()
    
        fmt.Println("wait goroutine")
    
        // 等待匿名goroutine
        <-ch
    
        fmt.Println("all done")
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    1. 循环接收
      通道的数据接收可以借用 for range 语句进行多个元素的接收操作,格式如下:

      for data := range ch {
      }

    通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。

    package main
    import (
    	"fmt"
    )
    
    func main() {
    
    	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)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    在遍历管道之前要先关闭管道,不然会出现deadlock的错误

    应用1

    在这里插入图片描述

    在这里插入图片描述

    开两个管道;
    当写协程完成工作之后,close数据管道,读协程对数据管道 intChan的数据读完之后,就向退出管道 exitChan 写入一个 true,close掉

    主线程循环检测退出管道里是否有数据,如果有,说明读协程完成,主程序就可以退出了。

    package main
    
    import (
    	"fmt"
    )
    
    //write Data
    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
    ", v)
    	}
    	//readData 读取完数据后,即任务完成
    	exitChan <- true
    	close(exitChan)
    
    }
    
    func main() {
    
    	//创建两个管道
    	intChan := make(chan int, 10)
    	exitChan := make(chan bool, 1)
    
    	go writeData(intChan)
    	go readData(intChan, exitChan)
    
    	//time.Sleep(time.Second * 10)
    	for {
    		_, ok := <-exitChan
    		if !ok {
    			break
    		}
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    应用2

    在这里插入图片描述
    在这里插入图片描述
    定义三个管道:
    intChan :放8000个数
    primeChan:放素数
    exitChan :4个协程运行完毕的标志

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    //向 intChan放入 1-8000个数
    func putNum(intChan chan int) {
    
    	for i := 1; i <= 80000; i++ {
    		intChan <- i
    	}
    
    	//关闭intChan
    	close(intChan)
    }
    
    // 从 intChan取出数据,并判断是否为素数,如果是,就
    // 	//放入到primeChan
    func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
    
    	//使用for 循环
    	// var num int
    	var flag bool //
    	for {
    		//time.Sleep(time.Millisecond * 10)
    		num, ok := <-intChan //intChan 取不到..
    
    		if !ok {
    			break
    		}
    		flag = true //假设是素数
    		//判断num是不是素数
    		for i := 2; i < num; i++ {
    			if num%i == 0 { //说明该num不是素数
    				flag = false
    				break
    			}
    		}
    
    		if flag {
    			//将这个数就放入到primeChan
    			primeChan <- num
    		}
    	}
    
    	fmt.Println("有一个primeNum 协程因为取不到数据,退出")
    	//这里我们还不能关闭 primeChan
    	//向 exitChan 写入true
    	exitChan <- true
    
    }
    
    func main() {
    
    	intChan := make(chan int, 1000)
    	primeChan := make(chan int, 20000) //放入结果
    	//标识退出的管道
    	exitChan := make(chan bool, 4) // 4个
    
    	start := time.Now().Unix()
    
    	//开启一个协程,向 intChan放入 1-8000个数
    	go putNum(intChan)
    	//开启4个协程,从 intChan取出数据,并判断是否为素数,如果是,就
    	//放入到primeChan
    	for i := 0; i < 4; i++ {
    		go primeNum(intChan, primeChan, exitChan)
    	}
    
    	//这里我们主线程,进行处理
    	//直接
    	go func() {
    		for i := 0; i < 4; i++ {
    			<-exitChan
    		}
    
    		end := time.Now().Unix()
    		fmt.Println("使用协程耗时=", end-start)
    
    		//当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
    		close(primeChan)
    	}()
    
    	//遍历我们的 primeChan ,把结果取出
    	for {
    		res, ok := <-primeChan
    		if !ok {
    			break
    		}
    		//将结果输出
    		fmt.Printf("素数=%d
    ", res)
    	}
    
    	fmt.Println("main线程退出")
    
    }
    
    
    有一个primeNum 协程因为取不到数据,退出
    有一个primeNum 协程因为取不到数据,退出
    有一个primeNum 协程因为取不到数据,退出
    有一个primeNum 协程因为取不到数据,退出
    使用协程耗时= 3
    main线程退出
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    存数字和计算素数比较简单,不提
    开启4个协程,运算素数,效率比单个线程高几倍!

    go func() {
    		for i := 0; i < 4; i++ {
    			<-exitChan
    		}
    
    		end := time.Now().Unix()
    		fmt.Println("使用协程耗时=", end-start)
    
    		//当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
    		close(primeChan)
    	}()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里定义了一个匿名协程,作用是检测4个协程 有没有完成运行,取不出来就会阻塞,等待协程完成。也可以这样:
    if len(exitChan) == 4

    最后

    深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

    因此收集整理了一份《Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

    小编已加密:aHR0cHM6Ly9kb2NzLnFxLmNvbS9kb2MvRFVrVm9aSGxQZUVsTlkwUnc==出于安全原因,我们把网站通过base64编码了,大家可以通过base64解码把网址获取下来。

  • 相关阅读:
    vue2+nuxt2实现服务端渲染(ssr),入门到项目
    @RequestBody,@RequestParam是否能随意改变入参字母大小写
    python正则表达式实战——获取图片
    [Qt] QtCreator编辑区关闭右侧不必要的警告提示
    TerraNoise for 3dMax插件教程
    如下面第一个图的九宫格中,放着 1~8 的数字卡片,还有一个格子空着。与空格子相邻的格子中的卡片可以移动到空格中。
    J. Counting Trees (树,卡特兰数)
    chmod的权限代号
    Python-使用openpyxl读取excel内容
    JVM笔记:GC 日志分析
  • 原文地址:https://blog.csdn.net/m0_67391401/article/details/126801763