golang——channel

目录

1.分类

channel的三种状态

channel的两种类型——有缓冲,无缓冲

无缓冲

有缓冲

2.操作

1.创建

2.发送

3.接收

4.关闭

3.使用场景

4.channel底层

5.channel线程安全

为什么是线程安全的

如何实现线程安全

6.channel控制goroutine并发执行顺序

7.channel共享内存的优缺点

8.channel发送和接收什么时候会死锁


1.分类

groutine的通信机制,每个channel都有类型

ch:=make(chan int)
//ch为make创建的底层数据结构的引用,零值为nil
  • 2种类型:无缓冲、有缓冲
  • 3种模式:写操作模式(单向通道)、读操作模式(单向通道)、读写操作模式(双向通道)

写操作模式

读操作模式

读写操作模式

创建

make(chan<-int)

make(<-chan int)

make(chan int)

channel的三种状态

未初始化

关闭

正常

关闭

panic

panic

正常关闭

发送

永远阻塞导致死锁

panic

阻塞/成功发送

接收

永远阻塞导致死锁

缓冲区为空则为零值,否则可以继续读且返回true

阻塞/成功接收

channel的两种类型——有缓冲,无缓冲

无缓冲有缓冲
创建方式make(chan TYPE)make(chan TYPE)
发送阻塞数据接收前发送阻塞缓冲区满时发送阻塞
接收阻塞数据发送前接收阻塞缓冲区空时接收阻塞

无缓冲

时刻都是同步状态

无缓冲chan必须有发送G/接收G的同时有接收G/发送G,否则就会阻塞,

报错:fatal error: all goroutines are asleep - deadlock!      

package main

import (
	"fmt"
	"time"
)

func loop(ch chan int) {
	for {
		select {
		case i := <-ch:
			fmt.Println("this is value of unbuffer channel", i)
		}
	}
}
func main() {

	ch := make(chan int)
	ch <- 1
	go loop(ch)
	time.Sleep(1 * time.Millisecond)

}

但如果把ch <-放到go loop(ch)下面,程序就会正常运行

有缓冲

package main

import (
	"fmt"
	"time"
)

func loop(ch chan int) {
	for {
		select {
		case i := <-ch:
			fmt.Println("this is value of unbuffer channel", i)
		}
	}
}
func main() {

	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	ch <- 4
	go loop(ch)

	time.Sleep(1 * time.Millisecond)

}

也会报错:fatal error: all goroutines are asleep - deadlock!      

是因为channel的大小为3,而要往里面塞4个数据,所以会阻塞住

解决方法:

1.把channel的长度调大(不是很好的解决方案)

2.把发送者ch<-1等代码移动到go loop(ch)下面,让channel实时消费就不会导致阻塞了

注意点:

一个chan不能多次关闭,会导致panic

如果多个goroutine都监听同一个channel,那么channel上的数据都可能随即被某一个goroutine取走进行消费

如果多个goroutine都监听同一个channel,如果这个channel被关闭,则所有goroutine都能收到退出信号

2.操作

1.创建

带缓冲
ch := make(chan int,3)
不带缓冲
ch := make(chan int)

创建时会做一些检查:

  • 元素大小不能超过64K
  • 元素对齐大小不能超过maxAlign(8字节)
  • 计算出来的内存是否超过限制

创建时的策略:

  • 无缓冲的channel——会直接给hchan分配内存
  • 有缓冲的channel并且元素不包含指针(buf指针,指向底层数组)——会为hchan和底层数组分配一段连续的地址
  • 有缓冲的channel并且元素包含指针——会为hchan和底层数组分别分配地址

2.发送

包括检查数据发送两个步骤

数据发送步骤

1.如果channel的读等待队列存在接收者goroutine(有发送者goroutine阻塞)

        将数据直接发送给第一个等待的goroutine,唤醒接收的goroutine

2.如果channel的读等待队列不存在接收者goroutine(无有发送者goroutine阻塞)

        如果buf指向的循环数组未满,会把数据发送到循环数组的队尾

        如果buf指向的循环数组已满,就会阻塞,将当前goroutine加入写等待队列,并挂起等待唤醒

func chansend(c *hchan,ep unsafe.Pointer,block bool,callerpc uintptr)bool

阻塞式:

调用chansend函数,且block=true

ch <- 10

非阻塞式:

调用chansend函数,且block=false

通过select让其在将阻塞时直接返回

select {
    case ch <- 10:
    ...
  default
}

3.接收

包括检查数据接收两个步骤

数据接收步骤

1.如果channel的写等待队列存在发送者goroutine(有发送者goroutine阻塞)

       如果是无缓冲channel,直接从第一个发送者goroutine那里把数据拷贝给接收变量,唤醒发送的goroutine

       如果是有缓冲channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据拷贝到buf指向的循环数组队尾,唤醒发送的goroutine

2.如果channel的写等待队列不存在发送者goroutine(没有发送者goroutine阻塞)

        如果buf指向的循环数组非空,将循环数组的队首元素拷贝给接收变量

        如果buf指向的循环数组为空,这个时候就会阻塞,将当前goroutine加入读等待队列,并挂起等待唤醒

func chanrecv(c *hchan,ep unsafe.Pointer,block bool)(selected,received bool)

阻塞式:

调用chanrecv函数,且block=true

有4种方式

<-ch

v:= <-ch

v,ok := <-ch

//当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值
for i := range ch {
    fmt.Println(i)
}

非阻塞式:

调用chanrecv函数,且block=false

select {
    case  <- ch:
    ...
  default
}

4.关闭

调用closechan函数

func closechan(c *hchan)
close(ch)
package main

import (
	"fmt"
	"time"
	"unsafe"
)

//G1是发送者
//当G1向ch发送数据时,首先会对buf加锁,然后将task存储的数据copy到buf中,然后sendx++,然后释放对buf的锁
func sendTask(ch chan string) {
	taskList := []string{"this", "is", "a", "demo"}
	for _, task := range taskList {
		ch <- task
	}
}

//G2是接收者
//当G2消费数据时,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁
func recvTask(ch chan string) {
	for {
		task := <-ch                  //接收任务
		fmt.Println("received", task) //处理任务
	}

}
func main() {
	//chan是带缓冲,缓冲大小为4的channel
	//初始hchan结构体的buf为空,sendx和recvx均为0
	ch := make(chan string, 4)
	fmt.Println(ch, unsafe.Sizeof(ch))
	go sendTask(ch)
	go recvTask(ch)
	time.Sleep(1 * time.Second)

}

0xc000058060 8                                                                                                                                       
received this                                                                                                                                        
received is                                                                                                                                          
received a                                                                                                                                           
received demo

3.使用场景

  1. 停止信号监听
  2. 定时任务
  3. 生产方和消费方解耦
  4. 控制并发数

4.channel底层

Go channel是一个队列,遵循FIFO的原则,负责协程之间的通信(Go语言不提倡通过共享内存而通信,而是通过通信来实现内存共享),CSP(Communicating Sequential Process)并发模型,就是通过goroutine和channel来实现的

通过var声明/make函数创建的channel变量是一个存储在函数栈上的指针,占用8个字节,指向堆上的hchan结构体

 buf循环数组好处:消费数据后不需要移动,只用移动sendx,recvx下标

有锁,保证线程安全

type hchan struct {
	closed   uint32 //channel是否关闭的标志
	elemtype *_type //channel中的元素类型
	//channel中分为有缓冲和无缓冲两种
	//对于有缓冲的channel存储数据,使用了ring buffer(环形缓冲区)来缓存写入的数据
    //本质是循环数组

	//普通数组容量固定更适合指定的空间,弹出元素时,普通数组需要全部前移
	buf      unsafe.Pointer //指向底层循环数组的指针(环形缓冲区)
	qcount   uint           //循环数组中的元素数量
	dataqsiz uint           //循环数组的长度
	elemsize uint16         //元素的大小
	sendx    uint           //下一次写下标的位置
	recvx    uint           //下一次读下标的位置

	//尝试读取channel或向channel写入数据时被阻塞的goroutine
	recvq waitq           //下一次写下标的位置
	sendq waitq			  //下一次读下标的位置
	
	lock mutex     //互斥锁,保证读写channel时不存在并发竞争问题

	
}


hchan结构体的主要组成部分:

  • 用来保存goroutine之间传递数据的循环数组:buf
  • 用来记录此循环数组当前发送或接收数据的下标值:sendx和recvx
  • 用于保存向该chan发送和从该chan接收数据被阻塞的goroutine队列:sendq和recvq
  • 保证channel写入和读取数据时线程安全的锁:lock

等待队列:

双向链表,包含一个头结点和一个尾结点

每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里

type waitq struct{
	first *sudog
	last *sudog
}
type sudog struct{
	g *g
	next *sudog
	prev *sudog
	elem unsafe.Pointer
	c * hchan  //等待的是哪个channel
	...
}

5.channel线程安全

为什么是线程安全的

不同协程通过channel进行通信,本身的使用场景就是多线程,为了保证数据的一致性,必须实现线程安全

如何实现线程安全

channel的底层实现中,hchan结构体中采用Mutex锁保证读写安全

在对循环数组buf中的数据进行入队和出队操作时,必须优先获取互斥锁,才能操作channel数据

6.channel控制goroutine并发执行顺序

多个goroutine并发执行时,每个goroutine抢到处理器的时间点不一致,goroutine的执行本身不能保证顺序——代码中先写的goroutine并不能保证先执行

思路:使用channel进行通知,用channel去传递信息,从而控制并发执行顺序

从第x个协程中拿数据,通知第x+1个协程

package main

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

var wg sync.WaitGroup

func print(goroutine string, inchan chan struct{}, outchan chan struct{}) {
	time.Sleep(1 * time.Second) //模仿内部操作耗时
	select {
	case <-inchan://从第x个协程中拿数据
		fmt.Printf("%s\n", goroutine)//通知第x+1个协程
		outchan <- struct{}{}
	}
	wg.Done()
}
func main() {
	ch1 := make(chan struct{}, 1)
	ch2 := make(chan struct{}, 1)
	ch3 := make(chan struct{}, 1)
	ch1 <- struct{}{}

	wg.Add(3)

	var start = time.Now().Unix()

	go print("goroutine1", ch1, ch2)
	go print("goroutine2", ch2, ch3)
	go print("goroutine3", ch3, ch1)

	wg.Wait()

	end := time.Now().Unix()
	fmt.Printf("during:%d", end-start)

}
goroutine1
goroutine2
goroutine3
during:1  

串行耗时3s,而这里并行耗时1s

7.channel共享内存的优缺点

go设计思想:不要通过共享内存来通信,要通过通信来共享内存

通过发送消息进行同步常见例子:goCSP模型(Communication Sequential Process)

                               ->process1

process->channel  ->process2

                               ->process3

大部分语言采用的都是通过共享内存进行通信,然后通过互斥锁,CAS等操作保证并发安全

go引入了channel和goroutine实现CSP模型,将生产者和消费者进行了解耦,channel和消息队列很相似

优点:

        将生产者和消费者进行了解耦,降低并发当中的耦合

缺点:

        容易出现死锁

8.channel发送和接收什么时候会死锁

死锁:

1.单个协程永久阻塞

2.两个或两个以上协程的执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞的现象

channel死锁场景:

1.非缓存channel读写不能同时

2.缓存channel缓存满时写/缓存空时读

3.多个channel相互等待

1.非缓存channel读写不能同时

package main

import "fmt"

func main() {
	ch := make(chan int)
	ans := <-ch
	//ch <- 1

	fmt.Println(ans)

}
package main

func main() {
	ch := make(chan int)
	//ans := <-ch
	ch <- 1
	//fmt.Println(ans)

}
package main

import "fmt"

func main() {
	ch := make(chan int)
	ans := <-ch
	ch <- 1
	fmt.Println(ans)

}
package main

import "fmt"

func main() {
	ch := make(chan int)
    ch <- 1
	ans := <-ch
	fmt.Println(ans)

}

2.缓存channel缓存满时写/缓存空时读

package main

func main() {
	ch := make(chan int, 1)
	ch <- 1
	ch <- 2
	
}
package main

import "fmt"

func main() {
	ch := make(chan int, 1)
	fmt.Println(<-ch)

}

3.多个channel相互等待

主协程和子协程相互等待导致死锁

package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	//互相等对方造成死锁
	go func() {
		for {
			select {
			case num := <-ch1:
				fmt.Println("num=", num)
				ch2 <- 100
			}
		}
	}()

	for {
		select {
		case num := <-ch2:
			fmt.Println("num=", num)
			ch1 <- 200

		}
	}

}

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值