Golang-Channel基本概述

本文详细介绍了Go语言中的Channel,包括声明、初始化、读写操作、无缓冲与有缓冲通道的区别,以及如何在并发环境中使用。通过案例展示了Channel的基本用法、关闭通道、遍历通道和select语句的使用。同时,探讨了Channel的内部实现和阻塞机制,以及在并发控制中的重要作用。最后,提供了一个模拟爬虫的高级示例,说明了Channel在实际问题解决中的应用。
摘要由CSDN通过智能技术生成

Channel就是Golang用来完成消息通讯的数据类型。Go语言中,仍然可以使用共享内存的方式在多个协程间共享数据,只不过不推荐使用。

声明Channel

在chan的左右添加<-符号,分别表示只读通道和只写通道。

var c1 chan int        // 可读写的通道
var c2 chan<- float64  // 只写通道
var c3 <-chan int      // 只读通道

fmt.Printf("c1=%#v \n",c1)
fmt.Printf("c2=%#v \n",c2)
fmt.Printf("c3=%#v \n",c3)

/*
	c1=(chan int)(nil)
	c2=(chan<- float64)(nil)
	c3=(<-chan int)(nil)
*/

从上面可以看到只声明未初始化的通道值是nil,需要初始化之后才会分配存储空间,通道初始化使用make方法。 make方法的第二个参数定义了通道可以缓冲参数的个数。

c1 := make(chan int) // 初始化无缓冲的通道
c2 := make(chan float64,10) // 初始化可以缓冲10个元素的通道
fmt.Printf("c1=%#v \n",c1)
fmt.Printf("c2=%#v \n",c2)

/*
	c1=(chan int)(0xc00006a120)
	c2=(chan float64)(0xc000120000)
*/

如上面代码c1这种没有缓冲空间的通道,我们称为非缓冲通道;非缓冲通道,无论读写,都是堵塞的,都需要找到配对的操作方才能执行。c2称为有缓冲通道。

基本用法

写入和读取数据

case1:

初始化通道c1,并写入数据。

package main

func main() {
	c1 := make(chan int,10)// 初始化可以缓冲10个元素的通道
	c1 <- 10
}

case2:

我们往c1这个无缓冲通道中,写入数据,而c1没有读操作。报错

c1 := make(chan int) // 初始化无缓冲通道
c1 <- 10  // 往通道c1写值 报错
/*
	fatal error: all goroutines are asleep - deadlock!
*/

case3:

报错!!!注意无缓冲通道的读写必须位于不同的协程中。

c1 := make(chan int) // 初始化无缓冲通道
c1 <- 10  // 往通道c1写值 报错
<-c1
/*
	fatal error: all goroutines are asleep - deadlock!
*/

case4:

​ 正确使用无缓冲通道方式

package main

import (
	"fmt"
	"time"
)
func main() {
	c1 := make(chan int) // 初始化可以缓冲10个元素的通道
	go func() {
		fmt.Printf("c1=%#v \n", <-c1) // 读取c1中的数据,输出c1=10
	}()
	c1 <- 10  // 往通道c1写值
	time.Sleep(1*time.Second) //等待协程读取channel数据
}
/*
	c1=10
*/

case5:

读取通道的数据时,通道左边如果是一个变量,会返回通道中的元素; 如果是两个变量,第一个是通道中复制出来的元素,第二个是通道的状态。其中通道的状态为true时,通道未关闭,状态为false时,通道关闭。已经关闭的通道不允许再发送数据。

c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c1 <- 10  // 往通道c1写值

v,status := <- c1
fmt.Printf("v=%#v,status=%#v",v,status) 
/*
	v=10,status=true
*/

关闭通道

case1:

关闭通道的方法是close方法。调用close方法关闭c通道,然后继续往c通道发送数据会报错。

c := make(chan int ,5)
c<-10
close(c)
c<-5 // 往关闭的通道中发送数据会报错
/*
	panic: send on closed channel
*/

case2:

调用close方法关闭通道时,会给所有等待读取通道数据的协程发送消息。这是一个非常有用的特性。

c := make(chan int)

go func() {
   ret,status := <-c
   fmt.Printf("rountine 1 v=%#v,status=%#v \n",ret,status)
}()

go func() {
   ret,status := <-c
   fmt.Printf("rountine 2 v=%#v,status=%#v \n",ret,status)
}()

close(c)  
time.Sleep(1*time.Second) 

/*

rountine 2 v=0,status=false 
rountine 1 v=0,status=false 

*/

虽然通道可以关闭,但并不是一个必须执行的方法,因为通道本身会通过垃圾回收器,根据它是否可以访问来决定是否回收。

遍历通道

遍历通道内的所有数据

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan int, 5)
	for i := 0; i < 5; i++ {
		c <- i
	}
	go func() {
		for v := range c {
			fmt.Printf("v=%d \n", v)
		}
	}()

	time.Sleep(time.Second)

}

select

select是Golang中的控制结构,和其它语言的switch语句写法类似。不过select的case语句必须是通道的读写操作。

如果有多个 case 都可以运行,select 会随机选出一个执行,其他case不会执行。default在没有case可 执行时,总可以执行。

package main

import (
	"fmt"
)

func main() {
	c1 := make(chan int ,10)
	c2 := make(chan int ,10)
	c3 := make(chan int ,10)
	var i1, i2 ,i3 int
	var ok bool
	c1 <- 10
	c3 <- 20
	for{
		select {
		case i1 = <-c1:
			fmt.Printf("received i1=%d \n", i1) 
		case c2 <- i2:
			fmt.Printf("sent %d \n", i2 ) 
		case i3, ok = <-c3:
			if ok {
				fmt.Printf("received i3=%d \n", i3)
			} else {
				fmt.Printf("c3 is closed\n")
			}
		default:
			fmt.Printf("no communication\n")
			return
		}
	}
}
/*
	sent 0 
	sent 0 
	received i3=20 
	received i1=10 
	sent 0 
	sent 0 
	sent 0 
	sent 0 
	sent 0 
	sent 0 
	sent 0 
	sent 0 
	no communication
	进程 已完成,退出代码为 0
*/

当select语句没有case条件满足,且没有定义default语句时,当前select所在协程会陷入阻塞状态。

通过time.After( time.Second),方法在1s之后会给通道发送消息,完成对select的超时操作:

c1 := make(chan int)

select{
   case <- c1:
      fmt.Println("c1" )
   case <-time.After( 1* time.Second):
      fmt.Println("1s" )
}

select经常和for一起使用

done := make(chan int)
// 无限循环,直到满足某个条件,操作done通道,完成循环
for{
    select{
        case <- done:
            return
        default:
            //进行某些操作
    }
}

通道的特性

  1. 通道可以作为参数在函数中传递,当作参数传递时,复制的是引用。
  2. 通道是***并发安全***的。
  3. 同一个通道的发送操作之间是互斥的,必须一个执行完了再执行下一个。接收操作和发送操作一样。
  4. 缓冲通道的发送操作需要复制元素值,然后在通道内存放一个副本。非缓冲通道则直接复制元素值的副本到接收操作。
  5. 往通道内复制的元素如果是引用类型,则复制的是引用类型的地址。
  6. 缓冲通道中的值放满之后,再往通道内发送数据,操作会阻塞。当有值被取走之后,会优先通知最早被阻塞的goroutine,重新发送数据。如果缓冲通道中的值为空,再从缓冲通道中接收数据也会被阻塞,当有新的值到来时,会优先通知最早被堵塞的goroutine,再次执行接收操作。
  7. 非缓冲通道,无论读写,都是堵塞的,都需要找到配对的操作方才能执行。
  8. 对于nil通道,他的发送和接收操作会永远阻塞。

高级示例

​ 模拟爬虫

// 抓取网页内容
func crawl(url string) (result string) {
	time.Sleep(1*time.Second) // 睡眠1s钟模拟抓取完数据
	return url+":抓取内容完成 \n"
}

// 保存文件内容到本地
func saveFile(url string,limiter chan bool, exit chan bool) {
	fmt.Printf("开启一个抓取协程 \n")
	result := crawl(url)
	if result != "" {
		fmt.Printf(result)
	}
	<-limiter  // 通知限速协程,抓取完成
	if exit != nil {
		exit<-true // 通知退出协程,程序执行完成
	}
}

// urls是要爬取的地址,n并发goroutine限制
func doWork(urls []string,n int) {
	limiter := make(chan bool,n) // 限速协程
	exit := make(chan bool) // 退出协程
	for i,value := range urls{
		limiter <- true
		if i == len(urls)-1 {
			go saveFile(value,limiter,exit)
		}else{
			go saveFile(value,limiter,nil)
		}
	}
	<-exit
}
func main() {
	urls := []string{"https://www.lixiang.com/","https://www.so.com","https://www.baidu.com/","https://www.360.com/"}
	doWork(urls, 1)
}

通过limiter协程的缓冲区大小,控制协程并发数量。通过exit协程的阻塞,结束最终程序。

实现原理

Channel在Golang中用hchan结构体表示。

type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}
  • buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表
  • sendxrecvx用于记录buf这个循环链表中的发送或者接收的index
  • lock是个互斥锁。
  • recvqsendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表

缓存中按链表顺序存放,取数据的时候按链表顺序读取,符合FIFO的原则。

send/recv

注意:缓存链表每一步的操作,都需要加锁

每一步的操作可以细化为:

  • 第一,加锁
  • 第二,把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)。
  • 第三,释放锁

阻塞

Goroutine的并发编程模型基于GMP模型,简要解释一下GMP的含义:

**G:**表示goroutine,每个goroutine都有自己的栈空间,定时器,初始化的栈空间在2k左右,空间会随着需求增长。

**M:**抽象化代表内核线程,记录内核线程栈信息,当goroutine调度到线程时,使用该goroutine自己的栈信息。

**P:**代表调度器,负责调度goroutine,维护一个本地goroutine队列,M从P上获得goroutine并执行,同时还负责部分内存的管理。

Go的goroutine是用户态的线程,用户态的线程是需要自己去调度的,Go有运行时scheduler去完成调度。

goroutine的阻塞操作,实际上是调用send (ch <- xx)或者recv ( <-ch)的时候主动触发的。

看下面样例:

goroutine1 中

c := make(chan int,2)
c<-1
c<-2
// 已满
// 再放,阻塞
c<-3

这个时候goroutine1会主动调用Go的调度器,让goroutine1等待,并从让出M,让其他G去使用。

同时goroutine1也会被抽象成含有goroutine1指针和send元素的sudog结构体保存到hchan的sendq中等待被唤醒。

goroutine2 中

v:= <-c

goroutine2从缓存队列中取出数据,channel会将等待队列中的goroutine1推出,将goroutine1当时send的数据推到缓存中,然后调用Go的scheduler,唤醒goroutine2,并把goroutine2放到可运行的Goroutine队列中。

假如是先进行执行recv操作的G2会怎么样,同理倒推即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coding小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值