Go使用协程交替打印字符


Posted in Golang onApril 29, 2021

需求: 模拟两个协程,分别循环打印字母A和B。

分析: 要实现两个协程之间的交替协作,就必须用到channel通信机制,而channel正好是同步阻塞的。

半开方式

首先我们用一个channel变量来控制两个goroutine的交替打印:

func main() {
   exit := make(chan bool)
   ch1 := make(chan int)
   go func() {
      for i := 1; i <= 10; i++ {
         ch1 <- 0 //生产
         fmt.Println("A",i)
      }
      exit <- true
   }()
   go func() {
      for i := 1; i <= 10; i++ {
         <-ch1 //消费
         fmt.Println("B",i)
      }
   }()
   <-exit
}

结果发现打印出了ABBAABBA...的效果。

也就是我们控制了开始的次序,但没有控制结束的次序,发生了并发不安全的情况。

其实半开模式也可以用于某些场景下,如: 两个goroutine,在条件控制下,交替打印奇偶数:

func main() {
   exit := make(chan bool)
   ch1 := make(chan int)
   go func() {
      for i := 1; i <= 10; i++ {
         ch1 <- 0
         if i%2 == 0 {
            fmt.Println("A", i)
         }
      }
      exit <- true
   }()
   go func() {
      for i := 1; i <= 10; i++ {
         <-ch1
         if i%2 == 1 {
            fmt.Println("B", i)
         }
      }
   }()
   <-exit
}

封闭方式

接下来我们使用两个channel变量来模拟goroutine循环体的互斥问题。

func main() {
   exit := make(chan bool)
   ch1, ch2 := make(chan bool), make(chan bool)
   
   go func() {
      for i := 1; i <= 10; i++ {
         ch1 <- true
         fmt.Println("A", i)
         //在ch1和ch2之间是阻塞独占的
         <-ch2
      }
      exit <- true
   }()
   go func() {
      for i := 1; i <= 10; i++ {
         <-ch1
         fmt.Println("B", i)
         ch2 <- true
      }
   }()
   <-exit
}

我们在循环体首尾都使用了阻塞独占模式,两个chan交替释放控制权,达到了安全的协程交互控制。

再看看下面的Demo,同样的原理:

func main(){
   ch1 :=make(chan int)
   ch2 :=make(chan string)
   str :=[5]string{"a","b","c","d","e"}
   go func() {
      for i:=0;i<5;i++{
         ch1<-i
         fmt.Print(i+1)
         <-ch2
      }
   }()
   for _,v :=range str{
      <-ch1
      fmt.Print(v)
      ch2<-v
   }
}

缓冲模式

缓冲模式和封闭模式相似,只是封闭模式中,两个goroutine有明确的首尾角色。而缓冲模式的第一生产者交给了主协程,两个goroutine结构一样,轮式交换角色。

func main() {
   exit := make(chan bool)
   ch1, ch2 := make(chan bool,1), make(chan bool)
   ch1 <- true //生产(选择一个启动项)
   
   go func() {
      for i := 1; i <= 10; i++ {
         if ok := <-ch1; ok { //消费
            fmt.Println("A", 2*i-1)
            ch2 <- true //生产
         }
      }
   }()
   go func() {
      defer func() { close(exit) }()
      for i := 1; i <= 10; i++ {
         if ok := <-ch2; ok { //消费
            fmt.Println("B", 2*i)
            ch1 <- true //生产
         }
      }
   }()
   <-exit
}

结论:

Channel的本质就是同步式的生产消费模式

补充:go 让N个协程交替打印1-100

今天遇到一道面试题,开启N个协程,并交替打印1-100如给定N=3则输出:

goroutine0: 0

goroutine1: 1

goroutine2: 2

goroutine0: 3

goroutine1: 4

面试时没答案,虽过后研究参考了一些网上方法,并记录下来,先上代码

func print() {
	chanNum := 3                           // chan 数量
	chanQueue := make([]chan int, chanNum) // 创建chan Slice
	var result = 0                         // 值
	exitChan := make(chan bool)            // 退出标识
	for i := 0; i < chanNum; i++ {
		//	创建chan
		chanQueue[i] = make(chan int)
		if i == chanNum-1 {
			//	给最后一个chan写一条数据,为了第一次输出从第1个chan输出
			go func(i int) {
				chanQueue[i] <- 1
			}(i)
		}
	}
	for i := 0; i < chanNum; i++ {
		var lastChan chan int //    上一个goroutine 结束才能输出 控制输出顺序
		var curChan chan int  //	当前阻塞输出的goroutine
		if i == 0 {
			lastChan = chanQueue[chanNum-1]
		} else {
			lastChan = chanQueue[i-1]
		}
		curChan = chanQueue[i]
		go func(i int, lastChan, curChan chan int) {
			for {
				if result > 100 {
					//	超过100就退出
					exitChan <- true
				}
				//	一直阻塞到上一个输出完,控制顺序
				<-lastChan
				fmt.Printf("thread%d: %d \n", i, result)
				result = result + 1
				//	当前goroutine已输出
				curChan <- 1
			}
		}(i, lastChan, curChan)
	}
	<-exitChan
	fmt.Println("done")
}

1、第一个for循环创建chan

2、第二个for循环里的lastChan意思是,当前chan如果要打印数据,就必须得上一个chan打印完后才能打印。

这里假设N=2,chan索引为0,1,当索引1要输出,就阻塞到索引0的chan有数据为止,当自己打印完后往自己的chan中发送一个1,方便给依赖自己的chan 解除阻塞。

这里有个特殊的地方,当索引为0时,他的依赖索引chan就为chanQueue的长度-1,如果没有在创建Chan中的时候没有下面这一串代码就会造成死锁

if i == chanNum-1 {
 // 给最后一个chan写一条数据,为了第一次输出从第1个chan输出
 go func(i int) {
 chanQueue[i] <- 1
 }(i)
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。如有错误或未考虑完全的地方,望不吝赐教。

Golang 相关文章推荐
Go Gin实现文件上传下载的示例代码
Apr 02 Golang
go语言中fallthrough的用法说明
May 06 Golang
基于Golang 高并发问题的解决方案
May 08 Golang
go开发alertmanger实现钉钉报警
Jul 16 Golang
golang中的struct操作
Nov 11 Golang
Go语言读取txt文档的操作方法
Jan 22 Golang
如何利用golang运用mysql数据库
Mar 13 Golang
golang三种设计模式之简单工厂、方法工厂和抽象工厂
Apr 10 Golang
Go获取两个时区的时间差
Apr 20 Golang
GoFrame基于性能测试得知grpool使用场景
Jun 21 Golang
Go语言编译原理之变量捕获
Aug 05 Golang
golang goroutine顺序输出方式
Apr 29 #Golang
golang 在windows中设置环境变量的操作
解决golang在import自己的包报错的问题
golang import自定义包方式
golang 接口嵌套实现复用的操作
Apr 29 #Golang
浅谈Golang 嵌套 interface 的赋值问题
Apr 29 #Golang
Go 实现英尺和米的简单单位换算方式
Apr 29 #Golang
You might like
PHP表单提交表单名称含有点号(.)则会被转化为下划线(_)
2011/12/14 PHP
win7+apache+php+mysql环境配置操作详解
2013/06/10 PHP
使用PHP接收POST数据,解析json数据
2013/06/28 PHP
zf框架的session会话周期及次数限制使用示例
2014/03/13 PHP
Zend Framework教程之配置文件application.ini解析
2016/03/10 PHP
PHP创建/删除/复制文件夹、文件
2016/05/03 PHP
thinkphp项目如何自定义微信分享描述内容
2017/02/20 PHP
高性能Javascript笔记 数据的存储与访问性能优化
2012/08/02 Javascript
缓动函数requestAnimationFrame 更好的实现浏览器经动画
2012/12/07 Javascript
基于jquery的simpleValidate简易验证插件
2014/01/31 Javascript
Validform+layer实现漂亮的表单验证特效
2016/01/17 Javascript
ExtJS 4.2 Grid组件单元格合并的方法
2016/10/12 Javascript
详解微信小程序设置底部导航栏目方法
2017/06/29 Javascript
jQuery EasyUI结合zTree树形结构制作web页面
2017/09/01 jQuery
Vue实现表格批量审核功能实例代码
2019/05/28 Javascript
Vue中消息横向滚动时setInterval清不掉的问题及解决方法
2019/08/23 Javascript
浅述python2与python3的简单区别
2018/09/19 Python
Python matplotlib学习笔记之坐标轴范围
2019/06/28 Python
如何爬取通过ajax加载数据的网站
2019/08/15 Python
Python3 合并二叉树的实现
2019/09/30 Python
如何在django中运行scrapy框架
2020/04/22 Python
Django中FilePathField字段的用法
2020/05/21 Python
招商业务员岗位职责
2013/12/16 职场文书
机关门卫岗位职责
2013/12/30 职场文书
党校培训自我鉴定
2014/02/01 职场文书
处级领导干部四风问题自我剖析材料
2014/09/29 职场文书
庆祝儿童节标语
2014/10/09 职场文书
导游词怎么写
2015/02/04 职场文书
给男朋友的道歉短信
2015/05/12 职场文书
辛德勒的名单观后感
2015/06/03 职场文书
工作收入证明模板
2015/06/12 职场文书
在职证明书模板
2015/06/15 职场文书
小学班主任研修日志
2015/11/13 职场文书
python 定义函数 返回值只取其中一个的实现
2021/05/21 Python
通过shell脚本对mysql的增删改查及my.cnf的配置
2021/07/07 MySQL
Python Matplotlib绘制条形图的全过程
2021/10/24 Python