Golang channel介绍

go channel简介

go语言有一句很经典的话,不要通过共享内存来通信,而应该通过通信来共享内存;

传统多线程编程的局限

        在Java等多线程编程中,线程间通信的主流方式是通过共享内存来通信。具体来说,多个线程之间通过共享一个全局变量来访问同一块内存区域,写线程将数据放入变量所指定的内存,读线程从该变量读取数据。然而,这种方式很容易导致数据访问冲突的问题,即多个线程同时访问同一个变量时,可能会出现数据不一致的情况。为了解决这一问题,需要引入锁机制来保证同一时间点该数据只能被一个线程访问,但这又带来了新的问题,如死锁、线程阻塞以及上下文切换带来的额外开销。

Go语言的并发编程模型

        Go语言从进程、线程基础上发展出了更轻量级的协程(goroutine),并为其设计了一套独特的并发编程模型。Go语言的作者深受通信顺序进程(Communicating Sequential Process,CSP)思想的启发,并在Go语言中实践了这套思想。CSP认为,程序就是一组无共享状态进程的并行组合,进程间的通信和同步采用信道(在Go语言中即Channel)完成。

通过通信来共享内存的优势

        在Go语言中,通过通信来共享内存的方式主要体现在使用Channel这一数据结构上。Channel类似于一个管道,有数据的输入端和接收端,不同协程可以根据实际需求选择发送数据到Channel中或是从Channel中接收数据。这种方式的优势在于:

  1. 简化并发编程:Go语言通过Channel为开发者提供了一种优雅简单的并发编程工具,使得开发者在编写并发程序时不必再陷入到各种锁的操作中。
  2. 避免数据访问冲突:由于同一时间只有一个协程能够访问Channel里面的数据,因此可以避免多线程编程中的数据访问冲突问题。
  3. 提高程序的可读性和可维护性:使用Channel进行通信可以使程序的结构更加清晰,逻辑更加简单,从而提高程序的可读性和可维护性。

 channel基本使用

无缓冲Channel的使用

无缓冲channel是指没有缓冲区的channel,发送操作会阻塞发送方,直到另一方准备好接收数据。同样,接收操作也会阻塞接收方,直到有数据可以接收。

package main  
  
import (  
	"fmt"  
	"time"  
)  
  
func main() {  
	// 创建一个无缓冲的整型channel  
	c := make(chan int)  
  
	// 启动一个新的goroutine,用于从channel中接收数据  
	go func() {  
		num := <-c // 接收数据,阻塞直到有数据可以接收  
		fmt.Println(num)  
	}()  
  
	// 向channel中发送数据  
	c <- 1 // 发送数据,阻塞直到另一方准备好接收  
  
	// 为了保证goroutine有足够的时间去接收channel中的值,这里等待3秒钟  
	time.Sleep(time.Second * 3)  
	fmt.Println("main function return")  
}


//运行结果

1
main function return

Process finished with the exit code 0

有缓冲Channel的使用

有缓冲channel是指带有缓冲区的channel,发送操作会在缓冲区未满的情况下立即返回,接收操作也会在缓冲区不为空的情况下立即返回。

package main  
  
import (  
	"fmt"  
)  
  
func main() {  
	// 创建一个带有缓冲区大小为2的整型channel  
	c := make(chan int, 2)  
  
	// 向channel中发送数据  
	c <- 1  
	c <- 2  
  
	// 启动一个新的goroutine,用于从channel中接收数据  
	go func() {  
		for num := range c { // 使用range循环接收数据,直到channel被关闭  
			fmt.Println(num)  
		}  
	}()  
  
	// 继续向channel中发送数据,此时会阻塞,直到有接收方接收数据或缓冲区已满  
	c <- 3  
  
	// 为了保证goroutine有足够的时间去接收channel中的值,这里等待一段时间(实际应用中可能需要更复杂的同步机制)   
	time.Sleep(time.Second)  
  
	// 关闭channel(注意:向一个已关闭的channel发送数据会导致panic)  
	close(c)  
}

//运行结果
1
2
3

Process finished with the exit code 0

 单向channel

单向channel是一种限制channel使用方向的机制。通过单向channel,我们可以确保一个函数只能发送数据到channel中,或者只能从channel中接收数据,但不能同时进行这两种操作。

只发送的单向Channel

var sendChan chan<- int //这表示sendChan是一个只能发送整数的单向channel。

只接收的单向Channel 

var recvChan <-chan int  //这表示recvChan是一个只能接收整数的单向channel。
package main  
  
import "fmt"  
  
// 定义一个只发送整数的函数  
func sendData(out chan<- int) {  
    out <- 42 // 向channel发送数据  
}  
  
// 定义一个只接收整数的函数  
func receiveData(in <-chan int) {  
    data := <-in // 从channel接收数据  
    fmt.Println(data)  
}  
  
func main() {  
    // 创建一个双向的整型channel  
    ch := make(chan int)  
  
    // 启动一个新的goroutine来发送数据  
    go sendData(ch)  
  
    // 在主goroutine中接收数据  
    receiveData(ch)  
}

channel数据结构

hchan:channel 数据结构

type hchan struct {
    qcount   uint           
    dataqsiz uint           
    buf      unsafe.Pointer 
    elemsize uint16
    closed   uint32
    elemtype *_type 
    sendx    uint   
    recvx    uint   
    recvq    waitq 
    sendq    waitq  
    
    lock mutex
}
  • qcount:当前 channel 中存在多少个元素;
  • dataqsize: 当前 channel 能存放的元素容量;
  • buf:channel 中用于存放元素的环形缓冲区;
  • elemsize:channel 元素类型的大小;
  • closed:标识 channel 是否关闭;
  • elemtype:channel 元素类型;
  • sendx:发送元素进入环形缓冲区的 index;
  • recvx:接收元素所处的环形缓冲区的 index;
  • recvq:因接收而陷入阻塞的协程队列;
  • sendq:因发送而陷入阻塞的协程队列;

waitq

waitq:阻塞的协程队列

type waitq struct {
    first *sudog
    last  *sudog
}
  • first:队列头部
  • last:队列尾部

sudog 

sudog:用于包装协程的节点

type sudog struct {
    g *g

    next *sudog
    prev *sudog
    elem unsafe.Pointer // data element (may point to stack)

    isSelect bool

    c        *hchan 
}
  • g:goroutine,协程;
  • next:队列中的下一个节点;
  • prev:队列中的前一个节点;
  • elem: 读取/写入 channel 的数据的容器;
  • isSelect:标识当前协程是否处在 select 多路复用的流程中;
  • c:标识与当前 sudog 交互的 chan.

 往channel中写数据

  • 对于未初始化的 chan,写入操作会引发死锁
  • 对于已关闭的 chan,写入操作会引发 panic
func chansend1(c *hchan, elem unsafe.Pointer) {
    chansend(c, elem, true, getcallerpc())
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    if c == nil {
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }

    lock(&c.lock)

    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }

从channel中读取数据

对于未初始化的 chan,读取操作会引发死锁 

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    if c == nil {
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
        throw("unreachable")
    }
}

关闭channel

 关于channel的几点总结

  • 关闭一个末初始化的 channel 会产生 panic。
  • channel只能被关闭一次,对同一个channel重复关闭会产生 panic。
  • 向一个已关闭的 channel 发送消息会产生 panic。
  • 从一个已关闭的channel读取消息不会发生panic,会一直读取所有数据,直到零值。
  • channel可以读端和写端都可有多个goroutine操作,在一端关闭channel的时候,该channel读端的所有goroutine 都会收到channel已关闭的消息。
  • channel是并发安全的,多个goroutine同时读取channel中的数据,不会产生并发安全问题
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值