久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

Thread和goroutine兩種方式怎樣實現共享變量按序輸出

158次閱讀
沒有評論

共計 5569 個字符,預計需要花費 14 分鐘才能閱讀完成。

這期內容當中丸趣 TV 小編將會給大家帶來有關 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 = 1;
 public 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 開始的信號量, 初始信號量數量為 1 
private static Semaphore s1 = new Semaphore(1);
// s2、s3 信號量,s1 完成后開始, 初始信號數量為 0
private 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 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 main

import (
  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 緩存、公用內存。當然還存在編譯優化帶來的指令重排序問題,要想徹底解決必須從編程語言層面保證原子性、有序性。無論如何處理,要想保證臨界區變量的安全,總會存在一定性能損耗。

上述就是丸趣 TV 小編為大家分享的 Thread 和 goroutine 兩種方式怎樣實現共享變量按序輸出了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注丸趣 TV 行業資訊頻道。

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-08-25發表,共計5569字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 太湖县| 曲阜市| 顺昌县| 莆田市| 卓资县| 松江区| 盐城市| 福海县| 汉沽区| 冷水江市| 陆良县| 望谟县| 汪清县| 石景山区| 咸宁市| 清河县| 景德镇市| 深水埗区| 逊克县| 岳阳县| 瓮安县| 涞源县| 集贤县| 塔河县| 本溪市| 正蓝旗| 大埔区| 礼泉县| 宜兰市| 攀枝花市| 台北县| 怀化市| 如皋市| 宁陕县| 高陵县| 周口市| 新巴尔虎右旗| 星子县| 尚义县| 江西省| 财经|