go併發程式設計之美(一)
一、前言
在Java中多執行緒之間是通過共享記憶體進行通訊的,在go中多執行緒之間通訊是基於訊息的,go中的通道是go中多執行緒通訊的基石。
在java中建立的執行緒是與OS執行緒一一對應的,而在go中多個協程(goroutine)對應一個邏輯處理器,每個邏輯處理器與OS執行緒一一對應。
每個執行緒要執行必須要在就緒狀態情況下獲取cpu,而作業系統是基於時間片輪轉演算法來排程執行緒佔用cpu來執行任務的,每個OS執行緒被分配一個時間片來佔用cpu進行任務的執行。
在java中由於建立的執行緒與os執行緒一一對應,所以java中的每個執行緒佔用一個時間片來執行。而go中多個協程對應一個os 執行緒,也就是多個協程對應了一個時間片,go則使用自己的排程策略(非os的排程策略)來讓多個協程使用一個時間片來併發的執行。也就是go中存在兩級策略,一個是go語言層面的排程多個協程公用一個時間片,一個是os層面的排程多個邏輯處理器輪詢佔用不同的時間片。
二、建立協程
在java中建立一個執行緒:
public static void main(String[] args) { Thread thread = new Thread(() -> { //run method dosomthing }); thread.start(); }
注:如上程式碼建立了一執行緒並啟動,在java中存在使用者執行緒與deamon執行緒之分,當不存在使用者執行緒時候,jvm程序就退出了(而不管main函式所線上程是否已經結束).
在go中建立一個協程:
func main() { //開啟一個協程,執行匿名函式裡面的內容 go func(){ //do somthing fmt.Println("im a go 協程") }() //休眠10s time.Sleep(10 * time.Second) }
注意:如上通過go關鍵字開啟一個協程,執行匿名函式裡面的內容,這裡需要注意main函式所線上程需要休眠以下,以便等開啟的協程執行,這是因為go中只要main函式執行緒退出則程序就退出。
三、同步
在java中我們可以使用Semaphore、CountDownLatch、CyclicBarrier等進行多執行緒之間同步,比如下面例子:
public final static Semaphore SEMAPHORE = new Semaphore(0); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { try { //run method dosomthing System.out.println(Thread.currentThread().getName() + "done"); }finally { SEMAPHORE.release(); } },"thread-1"); thread1.start(); Thread thread2 = new Thread(() -> { try { //run method dosomthing System.out.println(Thread.currentThread().getName() + "done"); }finally { SEMAPHORE.release(); } },"thread-2"); thread2.start(); System.out.println("wait all sub thread end"); SEMAPHORE.acquire(2); System.out.println("all sub thread end"); }
在go中也有類似的工具類:
//建立同步器 var wg sync.WaitGroup func main() { //兩個訊號 wg.Add(2) //開啟一個協程,執行匿名函式裡面的內容 go func() { //訊號量減去1 defer wg.Done() //do somthing fmt.Println("im A go 協程") }() //開啟一個協程,執行匿名函式裡面的內容 go func() { //訊號量減去1 defer wg.Done() //do somthing fmt.Println("im B go 協程") }() fmt.Println("wait all sub thread end") wg.Wait() fmt.Println(" all sub thread end") }
四、通道
go中通道分為有緩衝和無緩衝的,本節我們看如何使用有緩衝通道實現生產消費模型
var wg sync.WaitGroup func printer(ch chan int) { for i := range ch { fmt.Println(i) } wg.Done() } func main() { //1為攜程建立等待 wg.Add(1) //2建立緩衝通道 ch := make(chan int ,10) //3開啟go協程 go printer(ch) //4寫入到通道 for i := 1; i < 100; i++ { ch <- i; } //5關閉協程 close(ch) fmt.Println("wait sub thread over") //6等待攜程結束 wg.Wait() fmt.Println("main thread over") }
如上程式碼2建立了一個可以緩衝10個int 元素的通道,程式碼3開啟一個執行緒用來從通道里面讀取資料,程式碼4在主執行緒裡面寫入資料到通道,程式碼5關閉通道(關閉後不能再向通道寫入資料,但是可以從中讀取)。
上面例子如果用Java來寫,首先需要建立一個併發安全的佇列,然後開啟一個生產執行緒寫入資料到佇列,開啟一個執行緒從佇列讀取元素。
五、總結
本文我們簡單的對比了Java和go中如何處理併發問題的,後面我們在逐個詳細的探討。