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 相关文章推荐
解决Golang time.Parse和time.Format的时区问题
Apr 29 Golang
浅谈golang package中init方法的多处定义及运行顺序问题
May 06 Golang
Golang二维数组的使用方式
May 28 Golang
再次探讨go实现无限 buffer 的 channel方法
Jun 13 Golang
试了下Golang实现try catch的方法
Jul 01 Golang
Go并发4种方法简明讲解
Apr 06 Golang
Golang流模式之grpc的四种数据流
Apr 13 Golang
Go语言安装并操作redis的go-redis库
Apr 14 Golang
golang用type-switch判断interface的实际存储类型
Apr 14 Golang
Golang 并发编程 SingleFlight模式
Apr 26 Golang
Golang 入门 之url 包
May 04 Golang
Golang入门之计时器
May 04 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
模仿OSO的论坛(一)
2006/10/09 PHP
PHP SQLite类
2009/05/07 PHP
php实现字符串首字母转换成大写的方法
2015/03/17 PHP
wamp服务器访问php非常缓慢的解决过程
2015/07/01 PHP
php实现统计目录文件大小的函数
2015/12/25 PHP
PHP下载文件的函数实例代码
2016/05/18 PHP
Symfony2获取web目录绝对路径、相对路径、网址的方法
2016/11/14 PHP
10个基于Jquery的幻灯片插件教程
2010/10/29 Javascript
JS实现鼠标单击与双击事件共存
2014/03/08 Javascript
两种方法实现在HTML页面加载完毕后运行某个js
2014/06/16 Javascript
jQuery结合HTML5制作的爱心树表白动画
2015/02/01 Javascript
JavaScript焦点事件、鼠标事件和滚轮事件使用详解
2016/01/15 Javascript
JavaScript+html5 canvas绘制的圆弧荡秋千效果完整实例
2016/01/26 Javascript
详解Angular4 路由设置相关
2017/08/26 Javascript
高性能的javascript之加载顺序与执行原理篇
2018/01/14 Javascript
vue-cli构建项目下使用微信分享功能
2018/05/28 Javascript
Bootstrap Table实现定时刷新数据的方法
2018/08/13 Javascript
通过cordova将vue项目打包为webapp的方法
2019/02/02 Javascript
vue自定义switch开关组件,实现样式可自行更改
2019/11/01 Javascript
JavaScript实现点击切换验证码及校验
2021/01/10 Javascript
解析Mac OS下部署Pyhton的Django框架项目的过程
2016/05/03 Python
python绘制条形图方法代码详解
2017/12/19 Python
python字符串string的内置方法实例详解
2018/05/14 Python
对Python3中bytes和HexStr之间的转换详解
2018/12/04 Python
Python HTML解析器BeautifulSoup用法实例详解【爬虫解析器】
2019/04/05 Python
django 微信网页授权登陆的实现
2019/07/30 Python
PyTorch: 梯度下降及反向传播的实例详解
2019/08/20 Python
Python如何实现Paramiko的二次封装
2021/01/30 Python
HTML5 Canvas像素处理使用接口介绍
2012/12/02 HTML / CSS
毕业生怎样写好自荐信
2013/11/11 职场文书
大学生职业生涯规划书参考模板
2014/03/05 职场文书
2014-2015学年工作总结
2014/11/27 职场文书
暑期工社会实践报告
2015/07/13 职场文书
吧主申请感言怎么写
2015/08/03 职场文书
班主任培训研修日志
2015/11/13 职场文书
Redis Lua脚本实现ip限流示例
2022/07/15 Redis