温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Thread和goroutine两种方式怎样实现共享变量按序输出

发布时间:2021-11-23 21:57:10 阅读:142 作者:柒染 栏目:云计算
GO开发者专用服务器限时活动,0元免费领,库存有限,领完即止! 点击查看>>

这期内容当中小编将会给大家带来有关Thread和goroutine两种方式怎样实现共享变量按序输出,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

背景

最近在看go的一些底层实现,其中印象最为深刻的是go语言创造者之一Rob Pike说过的一句话,不要通过共享内存通信,而应该通过通信来共享内存,其中这后半句话对应的实现是通道(channel),利用通道在多个协程(goroutine)之间传递数据。看到这里,我不禁产生了一个疑问,对于无状态数据之间的传递,通过通道保证数据之间并发安全没什么问题,但我现在有一个临界区或者共享变量,存在多线程并发访问。Go协程如何控制数据并发安全性?难道还有其它高招?带着这个疑问,我们看看Go是如何保证临界区共享变量并发访问问题。

下面我们通过一个经典的题目来验证线程和协程分别是如何解决的。

有三个线程/协程完成如下任务:1线程/协程打印1,2线程/协程打印2,3线程/协程打印3,依次交替打印15次。输出:123123123123123

 

java实现

java对于这个问题如何解决呢?首先要求依次输出,那么只要保证线程互相等待或者说步调一致即可实现上述问题。

如何实现步调一致呢?我知道的方法至少有三种,以下我通过三种实现方式来介绍Java线程是如何控制临界区共享变量并发访问。

 

  Synchronized实现 

通过Synchronized解决互斥问题; (wait/notifyAll)等待-通知机制控制多个线程之间执行节奏。实现方式如下:

public class Thread123 { public static void main(String[] args) throws InterruptedException {  Thread123 testABC = new Thread123();   Thread thread1 = new Thread(new Runnable() {    @Override    public void run() {     try {      for (int i = 0; i < 5; i++) {       testABC.printA();      }     } catch (InterruptedException e) {      e.printStackTrace();     }    }   });   Thread thread2 = new Thread(new Runnable() {    @Override    public void run() {     try {      for (int i = 0; i < 5; i++) {       testABC.printB();      }     } catch (InterruptedException e) {      e.printStackTrace();     }    }   });   Thread thread3 = new Thread(new Runnable() {    @Override    public void run() {     try {      for (int i = 0; i < 5; i++) {       testABC.printC();      }     } catch (InterruptedException e) {      e.printStackTrace();     }    }   });   thread1.start();   thread2.start();   thread3.start();   thread1.join();   thread2.join();   thread3.join(); } int flag = 1public synchronized void printA() throws InterruptedException {  while (flag != 1) {   this.wait();  }  System.out.print(flag);  flag = 2;  this.notifyAll(); } private synchronized void printB() throws InterruptedException {  while (flag != 2) {   this.wait();  }  System.out.print(flag);  flag = 3;  this.notifyAll(); } private synchronized void printC() throws InterruptedException {  while (flag != 3) {   this.wait();  }  System.out.print(flag);  flag = 1;  this.notifyAll(); }}
 

看到这段实现可能大家都会有如下两个疑问:

  • 为啥要用notifyAll,而没有使用notify?

“  

这两者其实是有一定区别的,notify是随机的通知等待队列中的一个线程,而notifyAll是通知等待队列中所有的线程。可能我们第一感觉是即使使用了notifyAll也是只能有一个线程真正执行,但是在多线程编程中,所谓的感觉都蕴藏着风险,因为有些线程可能永远也不会被唤醒,这就导致即使满足条件也无法执行,所以除非你很清楚你的线程执行逻辑,一般情况下,不要使用notify。有兴趣的话,上面例子,可以测试下,你就可以得知为什么不建议你用notify。

”  
  • 为啥要用while循环,而不是用更轻量的if?

“  

利用while的原因,从根本上来说是java中的编程范式,只要涉及到wait等待,都需要用while。原因是因为当wait返回时,有可能判断条件已经发生变化,所以需要重新检验条件是否满足。

”  
 

Lock实现

通过Lock解决多线程之间互斥问题; (await/signal)解决线程之间同步,当然这种实现方式和上一种效果是一样的。

public class Test { // 打印方式跟上一种方式一样,这里不在给出。 private int flag = 1; private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); private  void  print1() {  try {   lock.lock();   while (flag != 1) {    try {     this.condition1.await();    } catch (InterruptedException e) {     e.printStackTrace();    }   }   System.out.print("A");   flag = 2;   this.condition2.signal();  }finally {   lock.unlock();  } } private void  print2() {  try {   lock.lock();   while (flag != 2) {    try {     this.condition2.await();    } catch (InterruptedException e) {     e.printStackTrace();    }   }   System.out.print("B");   flag = 3;   this.condition3.signal();  }finally {   lock.unlock();  } } private void  print3() {  try {   lock.lock();   while (flag != 3) {    try {     this.condition3.await();    } catch (InterruptedException e) {     e.printStackTrace();    }   }   System.out.print("C");   flag = 1;   this.condition1.signal();  }finally {   lock.unlock();  } }
   

Semaphore实现

信号量获取和归还机制来保证共享数据并发安全,以下为部分核心代码;

// 以s1开始的信号量,初始信号量数量为1private static Semaphore s1 = new Semaphore(1);// s2、s3信号量,s1完成后开始,初始信号数量为0private static Semaphore s2 = new Semaphore(0);private static Semaphore s3 = new Semaphore(0);static class Thread1 extends Thread {      @Override      public void run() {         try {            for (int i = 0; i < 10; i++) {               s1.acquire();// s1获取信号执行,s1信号量减1,当s1为0时将无法继续获得该信号量               System.out.print("1");               s2.release();// s2释放信号,s2信号量加1(初始为0),此时可以获取B信号量             }         } catch (InterruptedException e) {            e.printStackTrace();      }   }}
 

其实除了以上方法,用CountDownLatch实现多个线程互相等待应该也是可以解决的,这里不在过多举例。

 

Go实现

在用Go的实现过程中,主要用到了三个知识点。1、先后启用了三个goroutine对共享变量进行操作; 2、一把互斥锁产生的三个条件变量对三个协程进行控制; 3、使用signChannel目的是为了不让goroutine过早结束运行。

package mainimport ( "log" "sync")func main()  { //声明共享变量 var  flag = 1 //声明互斥锁 var lock sync.RWMutex //三个条件变量,用于控制三个协程执行频率 cnd1 := sync.NewCond(&lock) cnd2 := sync.NewCond(&lock) cnd3 := sync.NewCond(&lock) //创建一个通道,用于控制goroutine过早结束运行 signChannel := make(chan struct{}, 3) //最大循环次数 max := 5 go func(max int) {  //本次goroutine执行完成之后释放  defer func() {   signChannel <- struct{}{}  }()  //循环执行  for i := 1; i <= max; i++ {   // 锁定本次临界环境变量修改   lock.Lock()   //通过for循环检测条件是否发生变化,类似于上面的while   for flag != 1 {    //等待    cnd1.Wait()   }   //输出   log.Print(flag)   //修改标识,释放锁、并对其它协程发送信号   flag = 2   lock.Unlock()   cnd2.Signal()  } }(max) go func(max int) {  defer func() {   signChannel <- struct{}{}  }()  for i := 1; i <= max; i++ {   lock.Lock()   for flag != 2 {    cnd2.Wait()   }   log.Print(flag)   flag = 3   lock.Unlock()   cnd3.Signal()  } }(max) go func(max int) {  defer func() {   signChannel <- struct{}{}  }()  for i := 1; i <= max; i++ {   lock.Lock()   for flag != 3 {    cnd3.Wait()   }   log.Print(flag)   flag = 1   lock.Unlock()   cnd1.Signal()  } }(max) <- signChannel <- signChannel <- signChannel}
 

可以看出这种实现方式也是通过锁和条件变量来控制临界区,这跟线程中Lock、await/signal实现方式没什么区别。(这是初次学习Go中互斥锁这块知识时,根据自己理解,编写的一种实现方式,如有问题,请多指教或者留言指正)

通过如上加锁和条件变量的机制解决了临界区变量并发安全问题,我们知道,之所以会如上出现并发问题,从源头上来说是硬件开发人员给软件开发人员挖的一个坑,为了提高并发性能,计算机出现了多核CPU,为了提高运算速度,CPU中又添加了高速缓存,这就导致多个CPU在做计算的时候缓存不能共享、交替执行,从而出现并发问题,无论线程、还是协程、解决思路很简单,通过加锁、禁用CPU缓存、公用内存。当然还存在编译优化带来的指令重排序问题,要想彻底解决必须从编程语言层面保证原子性 、有序性。无论如何处理,要想保证临界区变量的安全,总会存在一定性能损耗。

上述就是小编为大家分享的Thread和goroutine两种方式怎样实现共享变量按序输出了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注亿速云行业资讯频道。

亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

原文链接:https://my.oschina.net/u/1787735/blog/4558721

AI

开发者交流群×