Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Go语言 | 从并发模式看channel使用技巧

Go语言 | 从并发模式看channel使用技巧

作者头像
飞雪无情
发布于 2020-08-05 06:59:33
发布于 2020-08-05 06:59:33
85600
代码可运行
举报
运行总次数:0
代码可运行

最近重看MinIO的源代码,发现纠删码模式下读取数据盘的时候,使用了更简单的并发读取方式,以前看的时候没发现,查了下Git历史记录,发现是19年新改的,新的使用channel做标记的方式的确非常巧妙,简化了代码逻辑,值得我们学习。所以今天就开篇文章,介绍下channel在并发下的两个使用技巧。

赢者为王模式

这种并发模式并不稀奇,相信很多朋友都用到过。它的核心思想就是同时开几个协程做同样的事情,谁先搞定,我们就用谁的结果。在Go语言的channel支持下,我们很容易实现这种并发方式。

假设我们把同一份资源,存储在网络上的5个服务器上(镜像、备份等),然后我们现在需要获取这个资源,我们就可以同时开5个协程,访问这5个服务器上的资源,谁先获取到,我们就用谁的,这样就可以最快速度获取,排除掉网络慢的服务器。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
	txtResult := make(chan string, 5)
	go func() {txtResult <- getTxt("res1.flysnow.org")}()
	go func() {txtResult <- getTxt("res2.flysnow.org")}()
	go func() {txtResult <- getTxt("res3.flysnow.org")}()
	go func() {txtResult <- getTxt("res4.flysnow.org")}()
	go func() {txtResult <- getTxt("res5.flysnow.org")}()
	println(<-txtResult)
}

func getTxt(host string) string{
	//省略网络访问逻辑,直接返回模拟结果
	//http.Get(host+"/1.txt")
	return host+":模拟结果"
}

其中getTxt没有真实实现,只是一个模拟,但是通过以上示例已经可以说明赢者为王这种并发模式的使用。这种并发模式适合多个协程对同一种资源的读取,更概括的讲就是做同一件事情,只要有一个协程干成了就OK了。这种模式的优点主要有两个:1.可以最大程度减少耗时;提高成功率。

最终成功模式

这种并发模式我们自己可能遇到过,但是可能不是叫这个名字,这个名字是我自己起的,我觉得比较贴切。比如同时并发的从10个文件中成功读取任意5个文件,你可以开启5个协程,也可以开启3个,都随意,但是必须是成功读取了5个才算成功,否则就是失败。

这种模式MinIO也遇到了,它的解决方式就是我在开篇提到的非常好的技巧,现在我们就来介绍这种技巧。在介绍这种技巧前,我们先列举下其他的思路。

第一种思路: 先并发获取,存放起来,然后再一个个判断是否获取成功,如果有的没有成功再重新获取,而且获取的文件不能重复。这种方式是取到结果后进行判断是否成功,然后根据情况再决定是否重新获取,要去重,要判断,业务逻辑比较复杂。

第二种思路: 并发的时候就保证成功,里面可能是个for循环,直到成功为止,然后再返回结果。这种思路缺陷也很明显,如果这个文件损坏,那么就会一直死循环下去,要避免死循环,就要加上重试次数。

而MinIO的实现方式比较巧妙,它也是多协程,但是发现如果有文件读取不成功,他会通过channel的方式标记,换一个文件读取。因为一共10个文件呢,这个不行,换一个,不能在一个文件上等死,只要成功读取5个就可以了。

现在我们看下MinIO的这段代码,代码比较长,我尽可能删除一些无用的,但是为了保证可读性,还是会长一些,大家耐心看完,就学到了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Read reads from readers in parallel. Returns p.dataBlocks number of bufs.
func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) {
	newBuf := dst
	//省略不太相关代码
	var newBufLK sync.RWMutex

	//省略无关
	//channel开始创建,要发挥作用了。这里记住几个数字:
	//readTriggerCh大小是10,p.dataBlocks大小是5
	readTriggerCh := make(chan bool, len(p.readers))
	for i := 0; i < p.dataBlocks; i++ {
		// Setup read triggers for p.dataBlocks number of reads so that it reads in parallel.
		readTriggerCh <- true
	}

	healRequired := int32(0) // Atomic bool flag.
	readerIndex := 0
	var wg sync.WaitGroup
	// readTrigger 为 true, 意味着需要用disk.ReadAt() 读取下一个数据
	// readTrigger 为 false, 意味着读取成功了,不再需要读取
	for readTrigger := range readTriggerCh {
		newBufLK.RLock()
		canDecode := p.canDecode(newBuf)
		newBufLK.RUnlock()
		//判断是否有5个成功的,如果有,退出for循环
		if canDecode {
			break
		}
		//读取次数上限,不能大于10
		if readerIndex == len(p.readers) {
			break
		}
		//成功了,退出本次读取
		if !readTrigger {
			continue
		}
		wg.Add(1)
		//并发读取数据
		go func(i int) {
			defer wg.Done()
			//省略不太相关代码
			_, err := rr.ReadAt(p.buf[bufIdx], p.offset)
			if err != nil {
				//省略不太相关代码
				// 失败了,标记为true,触发下一个读取.
				readTriggerCh <- true
				return
			}
			newBufLK.Lock()
			newBuf[bufIdx] = p.buf[bufIdx]
			newBufLK.Unlock()
			// 成功了,标记为false,不再读取
			readTriggerCh <- false
		}(readerIndex)
		//控制次数,同时用来作为索引获取和存储数据
		readerIndex++
	}
	wg.Wait()

    //最终结果判断,如果OK了就正确返回,如果有失败的,返回error信息。
	if p.canDecode(newBuf) {
		p.offset += p.shardSize
		if healRequired != 0 {
			return newBuf, errHealRequired
		}
		return newBuf, nil
	}

	return nil, errErasureReadQuorum
}

以上代码虽然长,但是我做了注释,也比较容易理解了。现在再对这段逻辑进行解释下:

  1. 前提是从10个数据里读取任意5个
  2. 初始化的chan大小是10,但是通过for循环只存放了5个true
  3. 然后对chan循环读取数据,如果是true就开启go协程获取数据,如果是false就终止这次循环
  4. 当前在这之前还会判断下是否已经成功获取了5个,如果是的话,直接跳出整个for循环
  5. 通过readerIndex每次尝试获取一个数据,如果成功赛一个false到chan中,如果失败则塞个true
  6. 这样不成功的readerIndex不再尝试读取,失败了就通过true标记尝试读取下一个readerIndex
  7. 通过chan这种巧妙的方式不断循环,直到成功读取5个,或者把10个数据都读一遍为止
  8. 最终再基于是否成功读取到5个数据,做最终的判断,是返回成功数据,还是错误

利用channel来做标记和循环取数据,是一种非常好的方式,简化了代码逻辑,整体看起来非常清晰了,有兴趣的朋友可以看下MinIO原来的代码,感受会更强烈。

小结

以上主要是两种使用channel的技巧,这些技巧一些会靠自己熟能生巧,一些需要看别人的源代码学习,而阅读开源代码是一个非常不错的途径。

本文为原创文章,转载注明出处,欢迎扫码关注公众号flysnow_org或者网站 https://www.flysnow.org/ ,第一时间看后续精彩文章。觉得好的话,请顺手点个赞吧。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020年8月4日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Go语言|基于channel实现的并发安全的字节池
字节切片[]byte是我们在编码中经常使用到的,比如要读取文件的内容,或者从io.Reader获取数据等,都需要[]byte做缓冲。
飞雪无情
2020/08/22
1K0
GO 语言的并发模式你了解多少?
实际上,出现上述的情况,还是因为我们对于 GO 语言的并发模型和涉及的 GO 语言基础不够扎实,误解了语言的用法。
阿兵云原生
2023/10/24
3420
GO 语言的并发模式你了解多少?
Goroutine和Channel的的使用和一些坑以及案例分析
简单认识一下Go的并发模型 简单聊一下并发模型,下一篇会单独全篇聊聊多种并发模型,以及其演进过程。 硬件发展越来越快,多核cpu正是盛行,为了提高cpu的利用率,编程语言开发者们也是各显神通,Java的多线程,nodejs的多进程,golang的协程等,我想大家在平时开发中都应该在各自公司的监控平台上看到cpu利用率低到5%左右,内存利用率经常80%左右。 软件运行的最小单位是进程,当一个软件或者应用程序启动时我们知道操作系统为其创建了一个进程;代码运行的最小单位是线程,我们平时编程时写的代码片段在程序跑起
阿伟
2019/08/13
1.5K0
Golang学习笔记之并发.协程(Goroutine)、信道(Channel)
简单的理解一下,并发就是你在跑步的时候鞋带开了,你停下来系鞋带。而并行则是,你一边听歌一边跑步。 并行并不代表比并发快,举一个例子,当文件下载完成时,应该使用弹出窗口来通知用户。而这种通信发生在负责下载的组件和负责渲染用户界面的组件之间。在并发系统中,这种通信的开销很低。而如果这两个组件并行地运行在 CPU 的不同核上,这种通信的开销却很大。因此并行程序并不一定会执行得更快。 Go 原生支持并发。在Go中,使用 Go 协程(Goroutine)和信道(channel)来处理并发。
李海彬
2018/12/27
1.4K0
Golang学习笔记之并发.协程(Goroutine)、信道(Channel)
go语言协程实现原理初探
golang作为一门现代语言,有其独特之处,比如一个go func(){}()语句即可实现协程,但也存在一些让人诟病的地方,比如错误处理等等。但是想必人无完人,无物完物。我们今天聊聊golang的协程(也叫goroutine)。首先提到协程,我们会想到进程,线程,那么协程是什么呢?协程是一种用户态的线程,他可以由用户自行创建和销毁,不需要内核调度,创建和销毁不需要占用太多系统资源的用户态线程。所以通常情况下,对于大并发的业务,我们通常能创建数以万计的协程来并发处理我们的业务,而不用担心资源占用过多。所以go的协程的作用就是为了实现并发编程,它是由go自己实现的调度器实现资源调度,从而开发者不用太多关心并发实现,从而可以安心的写一些牛逼的业务代码。
金鹏
2024/02/07
6893
go语言协程实现原理初探
Go语言入门(七)goroutine和channel
需要注意&lt;-是在chan关键字的位置,&lt;-在chan左侧表示只读,在右侧表示只写
alexhuiwang
2020/09/24
2880
​Golang 并发编程指南
作者:dcguo,腾讯 CSIG 电子签开放平台中心 分享 Golang 并发基础库,扩展以及三方库的一些常见问题、使用介绍和技巧,以及对一些并发库的选择和优化探讨。 go 原生/扩展库 提倡的原则 不要通过共享内存进行通信;相反,通过通信来共享内存。 Goroutine goroutine 并发模型 调度器主要结构 主要调度器结构是 M,P,G M,内核级别线程,goroutine 基于 M 之上,代表执行者,底层线程,物理线程 P,处理器,用来执行 goroutine,因此维护了一个 gorout
腾讯技术工程官方号
2021/12/20
1.4K0
GO 语言处理并发的时候我们是选择sync还是channel
以前写 C 的时候,我们一般是都通过共享内存来通信,对于并发去操作某一块数据时,为了保证数据安全,控制线程间同步,我们们会去使用互斥锁,加锁解锁来进行处理
阿兵云原生
2023/10/24
2330
GO 语言处理并发的时候我们是选择sync还是channel
go的并发编程
如果了解了GMP模型之后,自然了解go的并发特点,协程之间都可能是多线程并发执行的,通过开协程就可以实现并发:
仙士可
2022/02/22
3970
go的并发编程
Golang异步编程方式和技巧
Golang基于多线程、协程实现,与生俱来适合异步编程,当我们遇到那种需要批量处理且耗时的操作时,传统的线性执行就显得吃力,这时就会想到异步并行处理。下面介绍一些异步编程方式和技巧。
用户2132290
2024/04/12
1.1K0
Golang异步编程方式和技巧
总结了才知道,原来channel有这么多用法!V2
这篇文章总结了channel的11种常用操作,以一个更高的视角看待channel,会给大家带来对channel更全面的认识。
大彬
2019/07/31
2K0
这些 channel 用法你都用起来了吗?
channel 通道是可以让一个 goroutine 协程发送特定值到另一个 goroutine 协程的通信机制。
阿兵云原生
2023/10/24
2720
这些 channel 用法你都用起来了吗?
go进阶(1) -深入理解goroutine并发运行机制
并发指的是同时进行多个任务的程序,Web处理请求,读写处理操作,I/O操作都可以充分利用并发增长处理速度,随着网络的普及,并发操作逐渐不可或缺 
黄规速
2023/02/27
4.1K0
go进阶(1) -深入理解goroutine并发运行机制
盘点Golang并发那些事儿之二
上一节提到,golang中直接使用关键字go创建goroutine,无法满足我们的需求。主要问题如下
PayneWu
2021/06/10
4930
盘点Golang并发那些事儿之二
Go语言并发之并发实现、多核CPU设置、多协程间的通信、select、多协程间的同步(二十一)
Go语言并发之并发实现、多核CPU设置、多协程间的通信、select、多协程间的同步 目录结构
友儿
2022/09/11
1K0
Go并发编程
百度Go语言优势,肯定有一条是说Go天生就有支持并发的优势,其他语言支持多线程并发,需要一定的门槛,基础的积累,学习多线程、进程语法。在Go中,就不需要考虑这些,原生提供goroutine(协程),自动帮你处理任务,
用户9022575
2021/10/01
5620
Go 专栏|并发编程:goroutine,channel 和 sync
原文链接: Go 专栏|并发编程:goroutine,channel 和 sync
AlwaysBeta
2021/09/16
6620
Go 专栏|并发编程:goroutine,channel 和 sync
channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
Michel_Rolle
2023/11/30
2.5K0
一篇文章带你了解Go语言基础之并发(channel)
Hi,大家好,我是码农,星期八,本篇继续带来Go语言并发基础,channel如何使用。
Go进阶者
2021/01/22
4890
15.Go语言-通道
通道(channel) ,就是一个管道,可以想像成 Go 协程之间通信的管道。它是一种队列式的数据结构,遵循先入先出的规则。
面向加薪学习
2022/09/04
5810
相关推荐
Go语言|基于channel实现的并发安全的字节池
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验