2011年4月28日 星期四

14. Scala 與新的語言功能 --平行處理

平行處理是一個非常重要的程式設計方法,你幾乎不會不遇到它,在未來越來越多複雜的程式應用中,平行處理一定跑不掉。

「平行處理,我會啊,Java裡面不就不包含有 thread,用它就沒錯了」

沒錯,Java 一開始出來時,在語言層級直接支援 thread,以那時的時空背景,確實是令人眼睛一亮的特色。

「Java有 thread、有 synchronized (lock),還有一大堆豐富的資料結構供我們使用,還不夠嗎?」

以上確實都是使用 Java 撰寫平行處理程式時很重要工具。
可是,你若曾經使用 Java 的 thread 來開發過程式,你會發現並不是那麼好寫。你會遇到不少狀況,不是程式不小心就給你停住(deadlock啦),要不就是資料有時對有時不對(發生 race condition 了),要讓資料穩穩的走,實在不容易。

以上的這些感覺非常正常,不單是你,這也是大家都困繞擾的問題,因此才會持續有平行處理的解決方案出現。

Java 直到現在仍無法擺脫平行處理的夢靨,不單單是 Java,平行處理幾乎是所有語言面對的共同難題。雖然到了 Java 7,Java 對平行處理的解決方案,仍然顯得不夠完整。

Concurrency 這麼難纏主要的問題出在於共享的資料上,各個 thread 在不同的時間去存取同一個資料,造成資料的不一致。
為要解決這個不一致的問題,lock、critical section 等各種解決方案老早就出現。

但有了這些解決方案,我們還是要很小心,執行以下的步驟
1. 在多個 thread 會共享到的 class中,一個個加上 synchronized 來保護我們的共享資料。
2. 避免加上太多的 synchronized,以免失去了concurrency的好處。

這在簡單的應用中不會有問題,可是你是否發現,當你的資料一多,或是資料結構間互相使用的關係變多時,問題就來了。synchronized 不好好檢查與運用,deadlock絕對有你的份。

你若上網查資料,你可能會找到許多在 Java 中開發 concurrency 的好方法,其中可能包括以下的 guideline
1.盡量使用 final 的資料
2.lock 的順序需要保持一致(避免 deadlock)
3.盡量使用 Local Thread 變數
4.盡量使用 stack 中的資料,也就是參數或 method 中的 local variable

有這麼多的建議,代表這個問題的困難程度。以上這些建議都是好的,都有機會解決你一些困擾,可是為甚麼還是困難重重?

Doug Lea在 Java 界是一個 concurrency 的大專家,java.util.concurrent 是他的傑作,你沒看錯,Java 附的 concurrent package是他做的。他的 Fork/Join framework 是 Java 7 解決 concurrency 的重要解決方案,你若繼續使用 Java 開發程式絕對不能錯過。

由 Doug Lea 仍持續加強它的 concurrency framework,你就知道 concurrency 多麼難以處理。

在各種語言中解決 concurrency,最被人稱許的,大概數 Erlang了。
Erlang 一開始就號稱使用在電信產業,那種需要特別穩定與超高數量級的資料處理中,Erlang 對於 concurrency 確實有他的一套。

Erlang 將 concurrency解決得這麼穩定,在於以下的手法
1.No shared state:不要有共享的資料。
2.Lightweight processes:使用 process 為主要的處理對象,這裡的 process 不是 OS 中的 process,請不要誤會。
3.Asynchronous message-passing:使用訊息傳遞作為不同 process 間的溝通機制
4.Mailboxes to buffer incoming messages:使用 mailbox 儲存 message
5.Mailbox processing with pattern matching:使用 pattern matching 來比對
上述這些手段就是所謂的 Actor Model,當然這種觀念不是 Erlang 所獨有。

除以上幾段外,Erlang 可以完整解決平行處理的問題,其手段還包含
1.儘量使用函數
2.儘量使用常數
3.沒有 lock
4.沒有 side-effect

這些觀念都非常的重要,都是寫好 concurrency 程式的重要概念,請努力記住。在你學習 Scala 的過程中,這些都是一再要提醒自己的事。

Concurrency 絕非天上掉下來的禮物就自然解決的,許多觀念與思考邏輯的改變,才是解決 concurrency 的不二法門。

網路上有許多文章號稱 Scala 的 actor model 完美解決 concurrency 問題,你若因為這些文章來學習 Scala,你會大失所望。 Actor model 不是解決 concurrency 的萬靈丹,你的觀念才是!

單純的以為 actor model 的 message passing 就可解決 concurrency 這個大問題,Doug Lea 還需要那麼辛苦幹嘛?!

再回頭看清楚 Erlang 處理 concurrency 的原則,actor model 是其一,更重要的在後面,函數、常數、沒有 lock、沒有 side-effect,請看清楚,多看幾遍,咀嚼這些的含意,為何 Erlang 還需強調這些東西。
Data sharing 是 concurrency 的問題來源,直接解決這個問題來源才是做好 concurrency 的根本。但我們的程式通常是攪和來、攪和去,運行在單一 thread 上當然可以,多個 thread,嗯嗯...。請注意,等候 method 的 return 也是一種 data sharing。

這裡順帶一提,因為 lock 很容易造成 deadlock,所以使用 synchronized 來 lock 資料的機制,在 Scala 語言已經被移除,Scala 以沒有 synchronized 這種語法,當然你還是可以使用其他的 method 加上。
怪吧!這麼好用的 lock 機制,竟然被移除,違反你以前的學習觀念吧?!

為何要移除 synchronized 呢?當然是有其原因。Scala知道共享式資料才是造成 concurrency 問題的最大元兇,所以降低資料共享才是真正解決 concurrency 的最正確作法。
在 Scala 中會一再要求你,盡量符合以下的規範
1.盡量使用不可變的資料:變數使用 immutable 與 val
2.盡量使用 function:最好是跟純函數式的語言一樣,使用沒有 side-effect 的函數
3.盡量不要資料共享
4.使用 actor model,並使用 asynchronous message-passing:在 actor 中使用非同步的訊息傳遞
5.不要 lock:既然上面都已做到,已經不需要使用 lock 來保護資料了(此時使用 lock 反而容易造成程式 deadlock 的可能性)

第4點的非同步傳遞訊息,就是不等待對方回應的訊息傳遞,訊息傳遞完畢後就回來做自己的事,不需等待對方回應。

「為何不等待對方回應這麼重要呢?」

因為呼叫方 (caller) 若等待被呼叫方 (callee) 回應,callee 為要達成 caller 所要求的事項,可能需要進行一大堆的工作,而這些工作可能包含 callee 回頭呼叫 caller 的工作,若 callee 回頭呼叫的方式也是需要等待Caller完成才會繼續執行,這時問題發生:
1. caller 呼叫 callee 並等待
2. callee 呼叫 aller 並等待
以上兩種情況一發生,那就叫 deadlock,無解了,程式自然停下來,乾瞪眼!

所以非同步的訊息傳送才會這麼重要。但實際狀況卻是「同步的訊息傳送」有時避免不了,這時,你需要謹慎使用「同步的訊息傳送」。

平行處理是一個大問題,可是卻也是一個非解不可的問題,Scala 準備好各種好用的工具與寫程式的準繩,就看你會不會好好利用。
若你順著 Scala 幫你準備的東西,其實平行處理並不像想像那麼難,而且會這些工具還會讓你程式開發的速度與品質大幅增加。

沒有留言:

張貼留言