Go语言进阶,详解并发中的通道机制

对于Go语言的并发,前面有几篇文章都有涉及到,如下:

Go语言并发比较二叉树(Binary Tree)icon-default.png?t=M85Bhttps://blog.csdn.net/weixin_41896770/article/details/127569147
Go语言进阶,闭包、指针、并发icon-default.png?t=M85Bhttps://blog.csdn.net/weixin_41896770/article/details/127547900但都只是一个示例,没有具体涉及到其中存在的一些问题,比如说很容易出现死锁等情况,这里重点讲解如何使用通道以及遇到的问题的处理方法。
先看个简单的创建通道,然后发送内容的代码:

package main

import "fmt"

func main() {
	ch := make(chan string)
	ch <- "你好,寅恪光潜"
	fmt.Println(<-ch)
}

看起来是没有问题,而且语法都是合规的,但是运行时会报错:

fatal error: all goroutines are asleep - deadlock!

这是比较常见的错误,也就是发生了死锁,信息显示goroutine没有启动,是睡眠的状态,当然这里没有使用到goroutine机制,那么换个角度来理解,这个通道虽然是申请了,由于是无缓冲通道,所以如果没有接收者的话,你发送到通道就会直接流出去(看成是水管),这样就没啥意义。

缓冲通道

那么可以定义为有长度的通道,这样的话就相当于创建一种有缓冲机制的通道,让通道可以暂存数据,如下:

func main() {
	ch := make(chan string, 1)//这里使其成为有缓冲的通道
	ch <- "你好,寅恪光潜"
	fmt.Println(<-ch)
}
//你好,寅恪光潜

goroutine机制

使用goroutine机制,创建一个接收通道去接收发送通道发送过来的值,如下:

func receiver(ch chan string) {
	r := <-ch
	fmt.Println("这里就接收到了发送来的值:", r)
}

func main() {
	ch := make(chan string)
	go receiver(ch)
	ch <- "你好,寅恪光潜"
	fmt.Println("发送成功")
}
//这里就接收到了发送来的值: 你好,寅恪光潜
//发送成功

如果是多个goroutine并发的话将如何处理?我们将内容先全部发送到通道,然后再读取出来:

package main

import (
	"fmt"
	"time"
)

func f(ch chan<- string, i int) {
	time.Sleep(time.Duration(i) * time.Second)
	s := fmt.Sprintf("寅恪光潜%d", i)
	ch <- s
}

func main() {
	ch := make(chan string, 10)
	fmt.Println(ch, len(ch))
	for i := 0; i < 4; i++ {
		go f(ch, i)
	}
	for ret := range ch {
		fmt.Println(ret)
	}
}

看起来好像是没有问题,启动goroutine发送内容到通道,然后遍历读取出通道的内容。不过在遍历完之后会报错:

0xc000062180 0
寅恪光潜0
寅恪光潜1
寅恪光潜2
寅恪光潜3
fatal error: all goroutines are asleep - deadlock!

错误原因是,读取完之后通道是空的,然后再读取(接收)的时候就会发生阻塞死锁。

sync.WaitGroup等待组

这个时候我们使用sync.WaitGroup等待组,这个等待组的作用可以保证在并发环境中完成指定数量的任务。里面有几个方法的用法说明如下:
Add(1) 计数器+1 ; Done() 计数器-1或者 Add(-1)也可以 ;Wait() 等待计数器一直减到0,比如上面是循环4次,4个任务,我们将修改为:

package main

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

var wg sync.WaitGroup

func f(ch chan<- string, i int) {
	time.Sleep(time.Duration(i) * time.Second)
	s := fmt.Sprintf("寅恪光潜%d", i)
	ch <- s
	defer wg.Done() //完成一个任务,计数器就减1
}

func main() {
	ch := make(chan string, 10)
	go func() {
		wg.Wait() //一直等待直到完成
		close(ch) //记得关闭通道
	}()
	for i := 0; i < 4; i++ {
		wg.Add(1) //每个任务开始,计数器就就加1
		go f(ch, i)
	}
	for ret := range ch {
		fmt.Println(ret)
	}
}

/*
寅恪光潜0
寅恪光潜1
寅恪光潜2
寅恪光潜3
*/

select多路复用

除了上面的等待组外,还有一种常用的方法,使用select来同步并发,select会一直等待,直到某个case的通信操作完成时,执行case分支对应的语句,这个看起来更简洁一点,如下:

func f(ch chan<- string, i int) {
	time.Sleep(time.Duration(i) * time.Second)
	s := fmt.Sprintf("寅恪光潜%d", i)
	ch <- s
}

func main() {
	ch := make(chan string, 10)
	for i := 0; i < 4; i++ {
		go f(ch, i)
	}
	for {
		select {
		case info := <-ch:
			println(info)
		default:
			time.Sleep(time.Second)
			fmt.Println("无数据")
		}
	}
}
/*
无数据
寅恪光潜0
寅恪光潜1
无数据
无数据
寅恪光潜2
寅恪光潜3
无数据
无数据
...
*/

然后会一直这样下去,这种情况是没有关闭通道,一直循环着,比如做聊天室这种一直监听的情况。

单向通道

上面介绍是双向通道,可以发送也可以接收,当然你也可以定义只接收或者只发送的单通道。

定义接收通道
type Receiver <-chan int
定义发送通道
type Sender chan<- int 

对于这个符号在左边还是右边,我个人是这样去理解记忆,<-chan表示从通道流出来,那就是需要拿东西接着,就是接收通道;chan<-表示指向通道,那就是往通道发送内容,就是发送通道。

var ch = make(chan int, 3) //带缓冲的通道
var sender Sender = ch //发送通道
var receiver Receiver = ch //接收通道 

看个示例:

package main

import (
	"fmt"
	"time"
)

type Sender chan<- int   //发送通道
type Receiver <-chan int //接收通道

func main() {
	var ch = make(chan int, 0) //无缓冲的通道
	var num int = 110

	go func() {
		var sender Sender = ch
		sender <- num //往发送通道发送数据
		fmt.Println("发送的数据:", num)
	}()

	go func() {
		var receiver Receiver = ch //接收通道里的数据
		fmt.Println("接收的数据:", <-receiver)
	}()
	//让main函数执行结束的时间延迟1秒
	time.Sleep(time.Second)
}
/*
接收的数据: 110
发送的数据: 110
*/
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

寅恪光潜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值