閱讀323 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《GO並發編程實戰》—— 隻會執行一次

現在,讓我們再次聚焦到sync代碼包。除了我們介紹過的互斥鎖、讀寫鎖和條件變量,該代碼包還為我們提供了幾個非常有用的API。其中一個比較有特色的就是結構體類型sync.Once和它的Do方法。

與代表鎖的結構體類型sync.Mutex和sync.RWMutex一樣,sync.Once也是開箱即用的。換句話說,我們僅需對它進行簡單的聲明即可使用,就像這樣:

1 var once sync.Once
2  
3 once.Do(func() { fmt.Println("Once!") })

如上所示,我們聲明了一個名為once的sync.Once類型的變量之後,立刻就可以調用它的指針方法Do了。

該類型的方法Do可以接受一個無參數、無結果的函數值作為其參數。該方法一旦被調用,就會調用被作為參數傳入的那個函數。從這一點看,該方法的功能實在是稀鬆平常。不過,重點並不在這裏。

我們對一個sync.Once類型值的指針方法Do的有效調用次數永遠會是1。也就是說,無論我們調用這個方法多少次,也無論我們在多次調用時傳遞給它的參數值是否相同,都僅有第一次調用是有效的。無論怎樣,隻有我們第一次調用該方法時傳遞給它的那個函數會被執行。請看下麵的示例:

01 func onceDo() {
02     var num int
03     sign := make(chan bool)
04     var once sync.Once
05     f := func(ii int) func() {
06         return func() {
07             num = (num + ii*2)
08             sign <- true
09         }
10     }
11     for i := 0; i < 3; i++ {
12         fi := f(i + 1)
13         go once.Do(fi)
14     }
15     for j := 0; j < 3; j++ {
16         select {
17         case <-sign:
18             fmt.Println("Received a signal.")
19         case <-time.After(100 * time.Millisecond):
20             fmt.Println("Timeout!")
21         }
22     }
23     fmt.Printf("Num: %d.\n", num)
24 }

在onceDo函數中,我們利用for語句連續三次異步的調用once變量的Do方法。這三次調用傳給Do方法的參數值都是相同的,都是變量fi所代表的匿名函數值。這個函數值的功能是先改變num變量的值再向非緩衝的sign通道發送一個true。變量num的值可以表示出once的Do方法被有效調用的次數,而通道sign則被用來傳遞代表了fi函數被執行完畢的信號。請注意,為了能夠精確的表達出fi函數是在哪一次(或哪幾次)調用once.Do方法的時候被執行的,我們在這裏使用了閉包。在每次迭代之初,我們賦給fi變量的函數值都是對變量f所代表的函數值進行閉包的一個結果值。我們使用變量ii作為f函數中的自由變量,並在閉包的過程中把for代碼塊中的變量i的值加1後再與該自由變量綁定在一起。這樣就生成了為當次迭代專門定製的函數fi。每次迭代中生成的fi函數在被執行的時候都會修改變量num的值。這些新的值不會出現重複,並且非常有助於我們倒推出所有的曾賦給自由變量的ii的值。這樣,我們就可以知道哪個(或哪些)fi函數被真正的執行了。

函數onceDo中的第二條for語句的作用是等待之前的那三個異步調用的完成。讀者可能已經發現,這兩條for語句的預設迭代次數是一致的。在第二條for語句中,我們使用了select語句,並且為針對sign通道的接收操作設定了超時時間(100毫秒)。這是為了當永遠無法從sign通道中接收元素值的時候不至於造成永久的阻塞。select語句中的每個case在被執行時都會打印出相應的內容。這有助於我們觀察程序的實際運行情況。最後,我們還會打印出num變量的值。據此,我們可以判斷在前麵幾次傳遞給Do方法的fi是否都被執行了。

在執行onceDo函數之後,我們會看到如下打印內容:

1 Received a signal.
2  
3 Timeout!
4  
5 Timeout!
6  
7 Num: 2.

上麵的打印內容表明,在成功從sign通道接收了一個元素值之後,出現了兩次接收操作超時的情況。我們不用考慮在對sign通道的接收操作開始之時匿名函數fi還沒有被執行完畢的情況。因為100毫秒的時間已經足夠執行它很多很多次的了。因此,這兩次接收操作超時應該是當時沒有正在為此等待的對sign通道的發送操作導致的(注意,sign是一個非緩衝通道)。綜上所述,我們可以初步判斷,傳遞給once.Do方法的匿名函數fi隻被執行了一次。並且,這僅有一次的執行的對象是在我們第一次調用該方法時傳遞給它的那個fi函數。

依據最後一行打印內容,我們可以證實上述判斷。num變量的值為2意味著它隻被修改了一次,並且是在自由變量ii為1的時候被修改的。這就可以證實,隻有在for循環的第一次迭代時傳遞給once.Do方法的那個fi函數被執行了。這也符合sync.Once類型及其指針方法Do的語義。

請注意,這個僅被執行一次的限製隻是針對單個sync.Once類型值來說的。換句話說,每個sync.Once類型值的指針方法Do都可以被有效的調用一次。

這個sync.Once類型的典型應用場景就是執行僅需執行一次的任務。例如,數據庫連接池的初始化任務。又例如,一些心跳檢測之類的實時監測任務。等等。

在一探sync.Once類型及其指針方法Do的內部實現之後,我們會有所發現:它們所提供的功能正是由前麵講到的互斥鎖和原子操作來實現的。這個實現並不複雜。其使用的技巧包括衛述語句、雙重檢查鎖定,以及對共享標記的原子讀寫操作。在熟知了本章講述的這些同步工具

之後,我們是否也能輕易設計出這樣簡單、有效的解決方案呢?

總之,sync.Once類型及其方法實現了“隻會執行一次”的語義。我們在需要完成隻需或隻能執行一次的任務的時候應該首先想到它。

最後更新:2017-05-23 12:31:38

  上一篇:go  Java IO: FileOutputStream
  下一篇:go  Java 集合教程