您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關Thread和goroutine兩種方式怎樣實現共享變量按序輸出,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
最近在看go的一些底層實現,其中印象最為深刻的是go語言創造者之一Rob Pike說過的一句話,不要通過共享內存通信,而應該通過通信來共享內存,其中這后半句話對應的實現是通道(channel),利用通道在多個協程(goroutine)之間傳遞數據。看到這里,我不禁產生了一個疑問,對于無狀態數據之間的傳遞,通過通道保證數據之間并發安全沒什么問題,但我現在有一個臨界區或者共享變量,存在多線程并發訪問。Go協程如何控制數據并發安全性?難道還有其它高招?帶著這個疑問,我們看看Go是如何保證臨界區共享變量并發訪問問題。
下面我們通過一個經典的題目來驗證線程和協程分別是如何解決的。
有三個線程/協程完成如下任務:1線程/協程打印1,2線程/協程打印2,3線程/協程打印3,依次交替打印15次。輸出:123123123123123
java對于這個問題如何解決呢?首先要求依次輸出,那么只要保證線程互相等待或者說步調一致即可實現上述問題。
如何實現步調一致呢?我知道的方法至少有三種,以下我通過三種實現方式來介紹Java線程是如何控制臨界區共享變量并發訪問。
通過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解決多線程之間互斥問題; (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();
}
}
信號量獲取和歸還機制來保證共享數據并發安全,以下為部分核心代碼;
// 以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 < 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的實現過程中,主要用到了三個知識點。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緩存、公用內存。當然還存在編譯優化帶來的指令重排序問題,要想徹底解決必須從編程語言層面保證原子性 、有序性。無論如何處理,要想保證臨界區變量的安全,總會存在一定性能損耗。
上述就是小編為大家分享的Thread和goroutine兩種方式怎樣實現共享變量按序輸出了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。