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\n", intChan, &intChan)


	//3. 向管道写入数据
	intChan<- 10
	num := 211
	intChan<- num
	intChan<- 50
	// //如果从channel取出数据后,可以继续放入
	<-intChan
	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*/)

}
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)

这句代码显示: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\n", newCat, newCat)
	//下面的写法是错误的!编译不通过
	//fmt.Printf("newCat.Name=%v", newCat.Name)
	//使用类型断言
	a := newCat.(Cat) 
	fmt.Printf("newCat.Name=%v", a.Name)
}

定义 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. 循环接收
    通道的数据接收可以借用 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)
	}
}

在遍历管道之前要先关闭管道,不然会出现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\n", 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
		}
	}

}

应用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\n", res)
	}

	fmt.Println("main线程退出")

}
有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
使用协程耗时= 3
main线程退出

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

go func() {
		for i := 0; i < 4; i++ {
			<-exitChan
		}

		end := time.Now().Unix()
		fmt.Println("使用协程耗时=", end-start)

		//当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
		close(primeChan)
	}()

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

  • 18
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Go语言channel、sync.WaitGroup和context是三个非常重要的并发编程工具。下面我将对它们进行详细介绍。 ### Channel Go语言channel是一种在多个goroutine之间进行通信的机制。也可以说,channel是一种数据结构,它可以让一个goroutine向另一个goroutine发送一个值,同时还可以让另一个goroutine从channel接收这个值。在Go语言,使用make函数创建一个channel。例如: ```go ch := make(chan int) ``` 这行代码创建了一个类型为int的channel。可以在goroutine使用ch <- value语句向channel发送一个整数,例如: ```go go func() { ch <- 1 }() ``` 可以使用value := <- ch语句从channel接收一个整数,例如: ```go value := <- ch ``` 这行代码会阻塞,直到有一个整数被发送到这个channel为止。需要注意的是,如果没有接收者,发送操作会一直阻塞,直到有接收者为止;如果没有发送者,接收操作也会一直阻塞,直到有发送者为止。 ### sync.WaitGroup sync.WaitGroup是Go语言的一个同步工具,它可以等待一组goroutine完成工作。在WaitGroup,每个goroutine的工作完成后,都需要调用Done方法。主goroutine可以在Wait方法上阻塞,等待所有的goroutine完成工作。例如: ```go var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { // do some work wg.Done() }() } wg.Wait() ``` 这行代码创建了一个WaitGroup,并且启动了10个goroutine进行工作。每个goroutine完成工作后,都会调用wg.Done方法,主goroutine在wg.Wait上阻塞,等待所有的goroutine完成工作。 ### context context是Go语言的一个用于传递请求范围数据的机制。在一个请求处理,可以使用context携带一些请求数据,同时也可以使用context取消请求处理。例如: ```go func handleRequest(ctx context.Context) { // do some work select { case <-ctx.Done(): // handle cancelation default: // continue working } } ``` 这行代码定义了一个处理请求的函数,该函数接收一个context参数。如果context被取消,处理请求的函数将会停止工作。例如: ```go ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(time.Second) cancel() }() handleRequest(ctx) ``` 这行代码创建了一个带有取消功能的context,并且启动了一个goroutine在1秒后取消context。handleRequest函数会使用这个context来处理请求,并且如果context被取消,handleRequest函数会立刻停止工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值