使用者指南簡介
現今主流運算的世界正在快速變化。如果您打開電腦機殼,看看裡面的構造,很可能會看到一個雙核心處理器,或者如果您擁有高階電腦,則會看到一個四核心處理器。我們現在都在多處理器系統上執行軟體。
我們今天和明天編寫的程式碼可能永遠不會在單處理器系統上執行:平行硬體已成為標準。但軟體並非如此,至少目前還不是。人們仍然建立單線程程式碼,即使它無法充分利用當前和未來硬體的全部效能。
我們今天編寫的程式碼可能永遠不會在單處理器系統上執行! |
一些開發人員會嘗試使用低階並行原語,例如線程以及鎖定或同步區塊。然而,很明顯,在應用程式層級使用的共享記憶體多線程方法造成的麻煩多於解決的問題。低階並行處理通常很難正確實作,而且也不太有趣。
隨著硬體的這種巨大變化,軟體也必然要發生巨大變化。更高階的並行和並行概念,例如映射/歸約、分支/合併、演員和資料流,為不同類型的問題領域提供了自然的抽象,同時充分利用了多核心硬體。
進入 GPars 的世界
認識 GPars,這是一個適用於 Java 和 Groovy 的開放原始碼並行和並行程式庫,它為您提供了許多高階抽象,讓您可以在 Groovy 中編寫並行程式碼(映射/歸約、分支/合併、非同步閉包、演員、代理人、資料流並行和其他概念),這可以使您的 Java 和 Groovy 程式碼輕鬆地實現並行和/或並行化。
透過 GPars,您的 Java 和/或 Groovy 程式碼可以輕鬆利用目標系統上所有可用的處理器。您可以同時執行多個計算、平行請求網路資源、安全地解決階層式分治問題、執行函數式映射/歸約或資料平行集合處理,或者圍繞 Actor 或資料流模型建構您的應用程式。
如果您正在 Groovy 中處理商業、開放原始碼、教育或其他類型的軟體專案,請下載二進制檔案或從 Maven 儲存庫整合它們並開始使用。編寫高度並行和/或並行的 Java 和 Groovy 程式碼的大門敞開著。盡情享受吧!
致謝
如果沒有許多個人投入時間、精力和專業知識來使 GPars 成為可靠的產品,這個專案就不可能達到目前的狀態。首先,應該提到核心團隊中的成員
-
Václav Pech
-
Dierk Koenig
-
Alex Tkachman
-
Russel Winder
-
Paul King
-
Jon Kerridge
-
Rafał Sławik
隨著時間的推移,許多其他人也貢獻了他們的想法、提供了有用的意見回饋或以某種方式幫助了 GPars。這個群體中也有許多人,多到無法一一列出他們的名字,但至少讓我們列出最活躍的
-
Hamlet d’Arcy
-
Hans Dockter
-
Guillaume Laforge
-
Robert Fischer
-
Johannes Link
-
Graeme Rocher
-
Alex Miller
-
Jeff Gortatowsky
-
Jiří Kropáček
-
Jim Northrop
非常感謝大家! |

入門使用者指南
一些假設
在我們開始之前,讓我們提出一些假設
-
您了解並使用 Groovy 和/或 Java:否則您就不會花費寶貴的時間來研究 Groovy 和/或 Java 的並行和並行程式庫。
-
您絕對想要編寫採用並行和並行概念的程式碼。
-
如果您沒有使用 Groovy,您已準備好支付使用 Java 不可避免的冗長稅。
-
您的程式碼目標是多核心硬體。
-
您知道並行程式碼中事情可能隨時發生,以任何順序發生,而且更有可能同時發生多件事情。
準備好了嗎?
有了這些假設,我們就可以開始了。
越來越明顯的是,像 JVM 提供的線程/同步/鎖定層級處理並行和並行太低階,無法安全和舒適地使用。
許多高階概念,例如 演員和 資料流已經存在相當長的時間。在多核心晶片成為硬體主流之前很久,至少在資料中心而非桌面上就已經在使用平行晶片電腦。
因此,現在是時候將這些更高階的抽象納入主流軟體產業了。
這就是 GPars 為 Groovy 和 Java 語言實現的功能,允許它們使用更高階的抽象,從而使並行軟體的開發更容易且不易出錯。
GPars 中提供的概念可以分為三類
-
程式碼層級協助工具 - 可以應用於程式碼庫的小部分的建構,例如單個演算法或資料結構,而無需對整體專案架構進行任何重大變更
-
平行集合
-
非同步處理
-
分支/合併 (分治法)
-
-
架構層級概念 - 設計專案結構時需要考慮的建構
-
Actors (演員)
-
通訊循序處理程序 (CSP)
-
資料流
-
資料並行
-
-
共享可變狀態保護 - 透過使用適當的抽象,可以避免目前 95% 以上的共享可變狀態使用。對於其餘 5% 的使用案例,良好的抽象仍然是必要的,也就是說,當無法避免共享可變狀態時。
-
Agents (代理人)
-
軟體交易記憶體(目前尚未在 GPars 中完全實作)
-
下載與安裝
GPars 現在作為 Groovy 的一部分發佈。因此,如果您有安裝 Groovy,您應該已經有 GPars。您的 GPars 確切版本當然取決於您使用的 Groovy 版本。
如果您還沒有 GPars,並且您確實有 Groovy,那麼也許您應該升級您的 Groovy!
如果您需要,您可以從這裡下載 Groovy,並從這裡下載 GPars。 |
如果您沒有安裝 Groovy,但透過使用依賴關係或可能只是擁有 groovy-all artifact 來使用 Groovy,那麼您將需要獲取 GPars。此外,如果您想使用與 Groovy 捆綁的版本不同的 GPars 版本,或者您有無法升級的舊版無 GPars 的 Groovy,您將需要獲取 GPars。下載 GPars 的方式是
-
從儲存庫下載 Artifact 並手動新增它和所有傳遞依賴。
-
在 Gradle、Maven 或 Ivy(或 Gant 或 Ant)建置檔案中指定依賴關係。
-
使用 Grapes (對於 Groovy 腳本特別有用)。
-
從這裡下載並安裝。
如果您正在建構 Grails 或 Griffon 應用程式,您可以使用適當的插件來為您取得我們的 jar 檔案。
GPars 的 Artifact
如上所述,GPars 現在以標準方式與 Groovy 一起發佈。但是,如果您必須手動管理此依賴項,GPars 成品位於主要的 Maven 儲存庫中,並且在 Codehaus 主要和快照儲存庫關閉之前位於其中。
發行版本可以在 Maven 主要儲存庫中找到,但目前,目前的開發版本 (SNAPSHOT) 位於 Codehaus 快照儲存庫中。我們正在將其移動到另一個位置。
若要從 Gradle 或 Grapes 使用 GPars,請使用以下規範
1
"org.codehaus.gpars:gpars:1.2.0"
在後一種情況下,您可能需要手動將我們的快照儲存庫新增到搜尋清單中。使用 Maven 時,依賴項為
1
2
3
4
5
<dependency>
<groupId>org.codehaus.gpars</groupId>
<artifactId>gpars</artifactId>
<version>1.2.0</version>
</dependency>
傳遞依賴
作為一個程式庫,GPars 依賴於 2.2.1 之後的 Groovy 版本。此外,必須提供 Fork/Join 並發程式庫。這是 Java 7 的標準配備。
GPars 2.0 將依賴於 Java 8,並且僅適用於 Groovy 3.0 及更高版本。
請訪問我們 GPars 網站上的整合頁面以了解更多詳細資訊。
Hello World 範例
設定完成後,請嘗試以下 Groovy 腳本以確認您的設定運作正常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import static groovyx.gpars.actor.Actors.actor
/**
* A demo showing two cooperating actors. The decryptor decrypts received messages
* and replies them back. The console actor sends a message to decrypt, prints out
* the reply and terminates both actors. The main thread waits on both actors to
* finish using the join() method to prevent premature exit, since both actors use
* the default actor group, which uses a daemon thread pool.
* @author Dierk Koenig, Vaclav Pech
*/
def decryptor = actor {
loop {
react { message ->
if (message instanceof String) reply message.reverse()
else stop()
}
}
}
def console = actor {
decryptor.send 'lellarap si yvoorG'
react {
println 'Decrypted message: ' + it
decryptor.send false
}
}
[decryptor, console]*.join()
您應該在主控台上收到訊息「解密訊息:Groovy 是並行的」。
若要使用 Java API 快速測試 GPars,請編譯並執行以下 Java 程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import groovyx.gpars.MessagingRunnable;
import groovyx.gpars.actor.DynamicDispatchActor;
public class StatelessActorDemo {
public static void main(String[] args) throws InterruptedException {
final MyStatelessActor actor = new MyStatelessActor();
actor.start();
actor.send("Hello");
actor.sendAndWait(10);
actor.sendAndContinue(10.0, new MessagingRunnable<String>() {
@Override protected void doRun(final String s) {
System.out.println("Received a reply " + s);
}
});
}
}
class MyStatelessActor extends DynamicDispatchActor {
public void onMessage(final String msg) {
System.out.println("Received " + msg);
replyIfExists("Thank you");
}
public void onMessage(final Integer msg) {
System.out.println("Received a number " + msg);
replyIfExists("Thank you");
}
public void onMessage(final Object msg) {
System.out.println("Received an object " + msg);
replyIfExists("Thank you");
}
}
請記住,您幾乎肯定必須將 Groovy 成品以及 GPars 成品新增到組建中。GPars 在 Java 應用程式中可能會以 Java 速度運作,但它仍然有一些對 Groovy 的編譯依賴性。
程式碼慣例
我們在程式碼範例中遵循某些慣例。理解這些慣例可以幫助您更好地閱讀和理解 GPars 程式碼範例。
-
leftShift 運算子 '<<' 已在 actors、agents 和 dataflow 運算式 (變數和串流) 上重載,表示傳送訊息或賦值。
myActor << 'message' myAgent << {account -> account.add('5 USD')} myDataflowVariable << 120332
-
在 actors 和 agents 上,預設的 call() 方法也已重載表示傳送。因此,傳送訊息給 actor 或 agent 可能看起來像常規方法呼叫。
myActor "message" myAgent {house -> house.repair()}
-
GPars 中的 rightShift 運算子 '>>' 具有當綁定時的含義。因此
myDataflowVariable >> {value -> doSomethingWith(value)}
將排程在 myDataflowVariable 綁定到一個值之後才執行閉包,並將該值作為參數。
用法
在範例中,我們傾向於靜態匯入常用的 factory 方法
-
GParsPool.withPool()
-
GParsPool.withExistingPool()
-
GParsExecutorsPool.withPool()
-
GParsExecutorsPool.withExistingPool()
-
Actors.actor()
-
Actors.reactor()
-
Actors.fairReactor()
-
Actors.messageHandler()
-
Actors.fairMessageHandler()
-
Agent.agent()
-
Agent.fairAgent()
-
Dataflow.task()
-
Dataflow.operator()
這更多的是關於風格偏好和個人品味的問題,但我們認為靜態匯入使程式碼更加簡潔易讀。
在 IDE 中設定
將 GPars jar 檔案新增到您的專案或在 pom.xml 中定義適當的依賴項應該足以讓您開始在 IDE 中使用 GPars。
GPars DSL 識別
免費的 Community Edition 和商業的 Ultimate Edition 中的 IntelliJ IDEA 都會識別 GPars 領域特定語言,完整的方法(如 eachParallel() 、 reduce() 或 callAsync())並驗證它們。 GPars 使用 Groovy DSL 機制,一旦將 GPars jar 檔案新增到專案中,該機制就會教導 IntelliJ IDEA DSL。
概念的適用性
GPars 提供了許多概念可供選擇。我們正在不斷建構和更新我們的文件,以幫助用戶為他們手頭的任務選擇正確的抽象層級。請參閱概念比較以了解詳細資訊。
為了簡要總結這些建議,以下是一些基本準則
-
您正在查看一個集合,需要使用許多精美的 Groovy 集合方法(如 each()、collect()、find() 等)來迭代或處理。假設處理集合的每個元素都獨立於其他項目,那麼使用 GPars 並行集合可能很合適。
-
如果您有一個可能在背景安全運行的長時間計算,請使用 GPars 中的非同步調用支援。由於 GPars 非同步函數可以組成,您可以快速並行化這些複雜的函數計算,而無需明確標記獨立計算。
-
假設您需要並行化一個演算法。您可以識別一組具有相互依賴關係的任務。這些任務通常不需要共享資料,但相反,某些任務可能需要等待其他任務完成後才能開始。現在,您可以準備好在程式碼中明確表達這些依賴關係。使用 GPars 資料流任務,您可以建立內部循序任務,每個任務都可以與其他任務同時執行。資料流變數和通道為任務提供了宣告其依賴關係和安全交換資料的能力。
-
也許您無法避免在邏輯中使用共享可變狀態。多個執行緒將存取共享資料並(其中一些)修改它。傳統的鎖定和同步方法感覺太危險或不熟悉?那麼請使用 agents 來包裝您的資料並序列化對它的所有存取。
-
您正在建構一個具有高並發需求的系統。調整此處的資料結構或任務並不能解決問題。您需要從頭開始建構架構,並考慮並發性。訊息傳遞可能是可行的方法。您的選擇可能包括
-
Groovy CSP 為您提供並行流程的高度確定性和可組合模型。模型是圍繞計算或流程的概念組織的,這些計算或流程同時運行並透過同步通道進行通訊。
-
如果您嘗試解決複雜的資料處理問題,請考慮使用 GPars 資料流運算子來建構資料流網路。這個概念是圍繞事件驅動的轉換組織的,這些轉換使用非同步通道連接到管線中。
-
如果您需要建構一個通用、高度並發且可擴充的架構,遵循物件導向範例,Actors 和 Active Objects 將會大放異彩。
-
現在,您可能對目前專案中使用的概念有了更好的了解。請查看我們的使用者指南中關於它們的更多詳細資訊。
新功能
下一個 GPars 1.3.0 版本在先前版本之上引入了幾項增強功能和改進,主要是在資料流領域。
查看 JIRA 發行說明。
專案變更
非同步函數
待定
平行集合
待定
Fork / Join
待定
Actors (演員)
-
遠端 actors
-
從活動物件傳播異常
資料流
-
遠端資料流變數和通道
-
資料流運算子接受可變數量的引數
-
Select 支援 @CompileStatic
Agent
-
遠端 agents
STM
待定
其他
-
將 JDK 依賴性提高到 1.7 版
-
將 Groovy 依賴性提高到 2.2 版
-
將 jsr-177y fork-join 池實作取代為 JDK 1.7 中的實作
-
移除了對 jsr-166y 的依賴性
Java API – 從 Java 使用 GPars
我保證使用 GPars 會讓人上癮。一旦您迷上了它,您將無法在沒有它的情況下編碼。如果世界強迫您用 Java 編寫程式碼,您仍然可以從 GPars 的許多功能中受益。
Java API 細節
GPars 的某些部分在 Java 中是不相關的,最好直接使用底層 Java 程式庫
-
並行集合 – 直接使用 jsr-166y 程式庫的 Parallel Array,直到 GPars 1.3.0 可用為止
-
Fork/Join – 直接使用 jsr-166y 程式庫的 Fork/Join 支援,直到 GPars 1.3.0 可用為止
-
非同步函數 – 直接使用 Java 執行器服務
GPars 的其他部分可以像從 Groovy 一樣從 Java 使用,儘管大多數人會錯過 Groovy DSL 功能。
Java API 中的 GPars 閉包
為了克服 Java 中缺少閉包作為語言元素的問題,並避免強迫使用者透過 Java API 直接使用 Groovy 閉包,我們提供了一些方便的包裝類別來幫助您定義回呼、actor 主體或 dataflow 任務。
-
groovyx.gpars.MessagingRunnable - 用於單引數回呼或 actor 主體
-
groovyx.gpars.ReactorMessagingRunnable - 用於 ReactiveActor 主體
-
groovyx.gpars.DataflowMessagingRunnable - 用於 dataflow 運算子的主體
這些類別可以在 GPars API 預期使用 Groovy 閉包的地方使用。
Actors (演員)
DynamicDispatchActor 以及 ReactiveActor 類別可以像在 Groovy 中一樣使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import groovyx.gpars.MessagingRunnable;
import groovyx.gpars.actor.DynamicDispatchActor;
public class StatelessActorDemo {
public static void main(String[] args) throws InterruptedException {
final MyStatelessActor actor = new MyStatelessActor();
actor.start();
actor.send("Hello");
actor.sendAndWait(10);
actor.sendAndContinue(10.0, new MessagingRunnable<String>() {
@Override protected void doRun(final String s) {
System.out.println("Received a reply " + s);
}
});
}
}
class MyStatelessActor extends DynamicDispatchActor {
public void onMessage(final String msg) {
System.out.println("Received " + msg);
replyIfExists("Thank you");
}
public void onMessage(final Integer msg) {
System.out.println("Received a number " + msg);
replyIfExists("Thank you");
}
public void onMessage(final Object msg) {
System.out.println("Received an object " + msg);
replyIfExists("Thank you");
}
}
在 Groovy 和 Java 中使用 GPars 之間有一些差異,但請注意回呼在原地實例化 MessagingRunnable 類別以取代 Groovy 閉包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import groovy.lang.Closure;
import groovyx.gpars.ReactorMessagingRunnable;
import groovyx.gpars.actor.Actor;
import groovyx.gpars.actor.ReactiveActor;
public class ReactorDemo {
public static void main(final String[] args) throws InterruptedException {
final Closure handler = new ReactorMessagingRunnable<Integer, Integer>() {
@Override protected Integer doRun(final Integer integer) {
return integer * 2;
}
};
final Actor actor = new ReactiveActor(handler);
actor.start();
System.out.println("Result: " + actor.sendAndWait(1));
System.out.println("Result: " + actor.sendAndWait(2));
System.out.println("Result: " + actor.sendAndWait(3));
}
}
便利的工廠方法
顯然,所有快速建構 actors 的基本 factory 方法都在您期望的地方提供。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import groovy.lang.Closure;
import groovyx.gpars.ReactorMessagingRunnable;
import groovyx.gpars.actor.Actor;
import groovyx.gpars.actor.Actors;
public class ReactorDemo {
public static void main(final String[] args) throws InterruptedException {
final Closure handler = new ReactorMessagingRunnable<Integer, Integer>() {
@Override protected Integer doRun(final Integer integer) {
return integer * 2;
}
};
final Actor actor = Actors.reactor(handler);
System.out.println("Result: " + actor.sendAndWait(1));
System.out.println("Result: " + actor.sendAndWait(2));
System.out.println("Result: " + actor.sendAndWait(3));
}
}
Agents (代理人)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import groovyx.gpars.MessagingRunnable;
import groovyx.gpars.agent.Agent;
public class AgentDemo {
public static void main(final String[] args) throws InterruptedException {
final Agent counter = new Agent<Integer>(0);
counter.send(10);
System.out.println("Current value: " + counter.getVal());
counter.send(new MessagingRunnable<Integer>() {
@Override protected void doRun(final Integer integer) {
counter.updateValue(integer + 1);
}
});
System.out.println("Current value: " + counter.getVal());
}
}
資料流並行
DataflowVariables 和 DataflowQueues 都可以在 Java 中順利使用。只需避免使用方便的重載運算符,直接使用方法,例如 bind 、whenBound、getVal 等。
您也可以繼續使用 dataflow 任務,將 Runnable 或 Callable 的實例傳遞給它們,就像 Groovy 閉包一樣。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.MessagingRunnable;
import groovyx.gpars.dataflow.DataflowVariable;
import groovyx.gpars.group.DefaultPGroup;
import java.util.concurrent.Callable;
public class DataflowTaskDemo {
public static void main(final String[] args) throws InterruptedException {
final DefaultPGroup group = new DefaultPGroup(10);
final DataflowVariable a = new DataflowVariable();
group.task(new Runnable() {
public void run() {
a.bind(10);
}
});
final Promise result = group.task(new Callable() {
public Object call() throws Exception {
return (Integer)a.getVal() + 10;
}
});
result.whenBound(new MessagingRunnable<Integer>() {
@Override protected void doRun(final Integer integer) {
System.out.println("arguments = " + integer);
}
});
System.out.println("result = " + result.getVal());
}
}
資料流運算子
下面的範例應說明 Groovy 和 Java 針對 dataflow 運算符的 API 之間的主要差異。
-
在接受通道列表以建立運算符或選擇器時,請使用便利的工廠方法
-
使用 DataflowMessagingRunnable 來指定運算符主體
-
呼叫 getOwningProcessor() 從主體內取得運算符,以便例如綁定輸出值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import groovyx.gpars.DataflowMessagingRunnable;
import groovyx.gpars.dataflow.Dataflow;
import groovyx.gpars.dataflow.DataflowQueue;
import groovyx.gpars.dataflow.operator.DataflowProcessor;
import java.util.Arrays;
import java.util.List;
public class DataflowOperatorDemo {
public static void main(final String[] args) throws InterruptedException {
final DataflowQueue stream1 = new DataflowQueue();
final DataflowQueue stream2 = new DataflowQueue();
final DataflowQueue stream3 = new DataflowQueue();
final DataflowQueue stream4 = new DataflowQueue();
final DataflowProcessor op1 = Dataflow.selector(Arrays.asList(stream1), Arrays.asList(stream2), new DataflowMessagingRunnable(1) {
@Override protected void doRun(final Object... objects) {
getOwningProcessor().bindOutput(2*(Integer)objects[0]);
}
});
final List secondOperatorInput = Arrays.asList(stream2, stream3);
final DataflowProcessor op2 = Dataflow.operator(secondOperatorInput, Arrays.asList(stream4), new DataflowMessagingRunnable(2) {
@Override protected void doRun(final Object... objects) {
getOwningProcessor().bindOutput((Integer) objects[0] + (Integer) objects[1]);
}
});
stream1.bind(1);
stream1.bind(2);
stream1.bind(3);
stream3.bind(100);
stream3.bind(100);
stream3.bind(100);
System.out.println("Result: " + stream4.getVal());
System.out.println("Result: " + stream4.getVal());
System.out.println("Result: " + stream4.getVal());
op1.stop();
op2.stop();
}
}
效能
一般而言,無論您是從 Groovy 或 Java 使用 GPars,其開銷都是相同的,而且通常都非常低。例如,GPars actors 可以與其他 JVM actor 選項(例如 Scala actors)正面競爭。
由於 Groovy 程式碼通常由於動態方法調用而比 Java 程式碼執行速度稍慢,因此您可以考慮用 Java 編寫程式碼以提高效能。
通常,在任務或 actor 主體內進行的數值運算或頻繁的細粒度方法調用可以通過重寫為 Java 來獲益。
先決條件
所有 GPars 集成規則都同樣適用於 Java 專案和 Groovy 專案。您只需要在您的專案中包含 Groovy 發行版 jar 檔案,就可以開始使用了。
您可能還想查看我們的 Java-Maven 範例專案,以獲取有關如何將 GPars 集成到基於 Maven 的純 Java 應用程式中的提示 – Java 範例 Maven 專案

資料並行使用者指南
專注於數據而不是流程有助於我們建立穩健的並行程式。作為程式設計師,您可以定義您的數據以及應應用於該數據的函數,然後讓底層機制處理數據。通常,將會建立一組並行任務並提交到線程池以進行處理。
在 GPars 中,GParsPool 和 GParsExecutorsPool 類使您可以訪問低階數據並行技術。GParsPool 類依賴於 JDK 7 中引入的 Fork/Join 實作,並提供出色的功能和效能。為那些仍然需要使用較舊的 Java 執行器的用戶提供了 GParsExecutorsPool。
GPars 低階數據並行涵蓋三個基本領域
-
同時處理集合
-
非同步執行函數(閉包)
-
執行 Fork/Join(分而治之)演算法
此處描述的 API 基於將 GPars 與 JDK7 結合使用。它可以與較新的 JDK 一起使用,但 JDK8 引入了 Streams 框架,該框架可以直接從 Groovy 使用,並且本質上取代了此處涵蓋的 GPars 功能。目前正在努力提供基於 JDK8 Streams 框架的此處描述的 API,以與 JDK8 及更高版本一起使用,從而提供簡單的升級途徑。 |
平行集合
處理數據通常涉及操縱集合。列表、數組、集合、映射、迭代器、字串。許多其他數據類型可以被視為項目的集合。處理此類集合的常見模式是依序逐個獲取元素,並為序列中的每個項目執行動作。
例如,採用 min 函數,該函數應返回集合的最小元素。當您在數字集合上呼叫 min 方法時,會建立一個變數(例如 minVal)來儲存到目前為止看到的最小值,並將其初始化為給定類型的合理值,因此例如對於整數和浮點數,這很可能為零。然後迭代集合的元素,因為每個元素都與儲存的值進行比較。如果某個值小於目前儲存在 minVal 中的值,則會更改 minVal 以儲存新看到的較小值。
處理完所有元素後,集合中的最小值將儲存在 minVal 中。
但是,這個簡單的解決方案在多核心和多處理器硬體上是完全錯誤的。在雙核心晶片上執行 min 函數最多可以利用該晶片 50% 的計算能力。在四核心上,它將僅為 25%。因此,在後一種情況下,此演算法實際上浪費了晶片 75% 的計算能力。
樹狀結構證明更適合並行處理。
在我們的範例中,min 函數不需要逐行迭代所有元素並將它們的值與 minVal 變數進行比較。相反,它可以依賴於我們硬體的多核心/多處理器特性。
例如,parallel_min 函數可以比較集合中相鄰值的對(或特定大小的元組),並將元組中的最小值提升到下一輪比較中。在不同的元組中搜尋 最小值
可以安全地並行進行,因此同一輪中的元組可以由不同的核心同時處理,而不會在線程之間發生競爭或爭用。
認識平行陣列
儘管 extra166y 庫不是 JDK7 的一部分,但它帶來了一個非常方便的抽象概念,稱為 並行數組,而 GPars 利用了這種機制來提供非常 Groovy API。
如前所述,目前正在努力根據 Streams 重寫 GPars API,以供 JDK8 及更高版本的用戶使用。當然,使用 JDK8 及更高版本的人可以直接從 Groovy 使用 Streams。
如何?
GPars 以多種方式利用 並行數組
實作。GParsPool 和 GParsExecutorsPool 類提供了常見 Groovy 迭代方法(如 each、collect、findAll 等)的並行變體。
1
def selfPortraits = images.findAllParallel{it.contains me}.collectParallel{it.resize()}
它還允許更函數式的 map/reduce 樣式的集合處理。
1
def smallestSelfPortrait = images.parallel.filter{it.contains me}.map{it.resize()}.min{it.sizeInMB}
GParsPool
使用 GParsPool — 基於 JSR-166y 的並行集合處理器
用法
GParsPool 類(來自 JSR-166y)為集合和物件提供了基於 ParallelArray 的並行 DSL。
使用範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Summarize numbers concurrently.
GParsPool.withPool {
final AtomicInteger result = new AtomicInteger(0)
[1, 2, 3, 4, 5].eachParallel{result.addAndGet(it)}
assert 15 == result
}
// Multiply numbers asynchronously.
GParsPool.withPool {
final List result = [1, 2, 3, 4, 5].collectParallel{it * 2}
assert ([2, 4, 6, 8, 10].equals(result))
}
傳入的閉包將 ForkJoinPool 的實例作為參數,然後可以在閉包內自由使用。
1
2
3
4
5
6
// Check whether all elements within a collection meet certain criteria.
GParsPool.withPool(5){ForkJoinPool pool ->
assert [1, 2, 3, 4, 5].everyParallel{it > 0}
assert ![1, 2, 3, 4, 5].everyParallel{it > 1}
}
GParsPool.withPool 方法採用可選參數,用於建立的池中的線程數,以及一個 未處理的異常
處理程序。
1
2
withPool(10){...}
withPool(20, exceptionHandler){...}
池重用
GParsPool.withExistingPool 採用已存在的 ForkJoinPool 實例來重用。DSL 僅在相關的程式碼區塊內且僅對於呼叫了 withPool 或 withExistingPool 方法的線程有效。withPool 方法僅在所有工作線程完成其任務並銷毀池後才返回,並返回相關程式碼區塊的結果值。withExistingPool 方法不會等待池線程完成。
或者,可以將 GParsPool 類靜態匯入為 import static groovyx.gpars.GParsPool,因此我們可以省略 GParsPool 類名稱。
1
2
3
4
withPool {
assert [1, 2, 3, 4, 5].everyParallel{it > 0}
assert ![1, 2, 3, 4, 5].everyParallel{it > 1}
}
目前在 Groovy 中的所有物件上支援以下方法
-
eachParallel
-
eachWithIndexParallel
-
collectParallel
-
collectManyParallel
-
findAllParallel
-
findAnyParallel
-
findParallel
-
everyParallel
-
anyParallel
-
grepParallel
-
groupByParallel
-
foldParallel
-
minParallel
-
maxParallel
-
sumParallel
-
splitParallel
-
countParallel
-
foldParallel
元類別增強器
作為替代方案,您可以使用 ParallelEnhancer 類來增強任何類別或個別實例的元類別,並使用並行方法。
1
2
3
4
5
6
7
8
9
10
import groovyx.gpars.ParallelEnhancer
def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
ParallelEnhancer.enhanceInstance(list)
println list.collectParallel {it * 2 }
def animals = ['dog', 'ant', 'cat', 'whale']
ParallelEnhancer.enhanceInstance animals
println (animals.anyParallel {it ==~ /ant/} ? 'Found an ant' : 'No ants found')
println (animals.everyParallel {it.contains('a')} ? 'All animals contain a' : 'Some animals can live without an a')
當使用 ParallelEnhancer 類時,您在使用 GParsPool DSL 時不會受限於 withPool 區塊。增強的類別或實例將保持增強,直到它們被垃圾回收。
異常處理
如果在處理任何傳入的閉包時引發異常,則會從 xxxParallel 方法重新拋出第一個異常,並且演算法會儘快停止。
透明的並行集合
除了新增 xxxParallel 方法之外,GPars 還可以讓您變更原始迭代方法的語意。
例如,您可能會將集合傳遞到程式庫方法中,該方法將以循序方式處理您的集合,例如,透過使用 collect 方法。然後,透過變更集合上 collect 方法的語意,您可以有效地並行化此程式庫循序程式碼。
1
2
3
4
5
6
7
8
9
10
11
12
GParsPool.withPool {
//The selectImportantNames() will process the name collections concurrently
assert ['ALICE', 'JASON'] == selectImportantNames(['Joe', 'Alice', 'Dave', 'Jason'].makeConcurrent())
}
/**
* A function implemented using standard sequential collect() and findAll() methods.
*/
def selectImportantNames(names) {
names.collect {it.toUpperCase()}.findAll{it.size() > 4}
}
makeSequential 方法會將集合重置為原始循序語意。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import static groovyx.gpars.GParsPool.withPool
def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
println 'Sequential: ' list.each { print it + ',' } println()
withPool {
println 'Sequential: '
list.each { print it + ',' }
println()
list.makeConcurrent()
println 'Concurrent: '
list.each { print it + ',' }
println()
list.makeSequential()
println 'Sequential: '
list.each { print it + ',' }
println()
}
println 'Sequential: '
list.each { print it + ',' }
println()
asConcurrent() 便利方法允許我們指定程式碼區塊,其中集合保持並行語意。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import static groovyx.gpars.GParsPool.withPool
def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
println 'Sequential: '
list.each { print it + ',' }
println()
withPool {
println 'Sequential: '
list.each { print it + ',' }
println()
list.asConcurrent {
println 'Concurrent: '
list.each { print it + ',' }
println()
}
println 'Sequential: '
list.each { print it + ',' }
println()
}
println 'Sequential: '
list.each { print it + ',' }
println()
程式碼範例
透明的並行性,包括 makeConcurrent()、makeSequential() 和 asConcurrent() 方法,也可與我們的 ParallelEnhancer 結合使用。
1
2
3
4
5
6
7
8
9
10
11
12
/**
* A function implemented using standard sequential collect() and findAll() methods.
*/
def selectImportantNames(names) {
names.collect {it.toUpperCase()}.findAll{it.size() > 4}
}
def names = ['Joe', 'Alice', 'Dave', 'Jason']
ParallelEnhancer.enhanceInstance(names)
//The selectImportantNames() will process the name collections concurrently
assert ['ALICE', 'JASON'] == selectImportantNames(names.makeConcurrent())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import groovyx.gpars.ParallelEnhancer
def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
println 'Sequential: '
list.each { print it + ',' }
println()
ParallelEnhancer.enhanceInstance(list)
println 'Sequential: '
list.each { print it + ',' }
println()
list.asConcurrent {
println 'Concurrent: '
list.each { print it + ',' }
println()
}
list.makeSequential()
println 'Sequential: '
list.each { print it + ',' }
println()
避免函式中的副作用
我們必須警告您。由於提供給並行方法(例如 eachParallel 或 collectParallel())的閉包可以並行執行,因此您必須確保每個閉包都以線程安全的方式編寫。閉包不得持有內部狀態、共享數據或具有超出其所調用單個元素邊界的副作用。違反這些規則將打開競爭條件和死鎖的大門,這是現代多核心程式設計師最嚴重的敵人。
不要這樣做! |
1
2
def thumbnails = []
images.eachParallel {thumbnails << it.thumbnail} //Concurrently accessing a not-thread-safe collection of thumbnails? Don't do this!
至少,您已經被警告了。
GParsExecutorsPool
使用 GParsExecutorsPool - 基於 Java Executors
的並行集合處理器 -
GParsExecutorsPool 的用法
GParsPool 類別為集合和物件啟用基於 Java Executors
的並行 DSL。
GParsExecutorsPool 類別可用作純基於 JDK 的 集合並行處理器
。與 GParsPool 類別不同,GParsExecutorsPool 不需要 fork/join 執行緒池,而是利用標準 JDK 執行器服務來並行化閉包,以迭代處理集合或物件。
然而,需要說明的是,GParsPool 通常比 GParsExecutorsPool 執行得更好。
GParsPool 通常比 GParsExecutorsPool 執行得更好 |
使用範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//multiply numbers asynchronously
GParsExecutorsPool.withPool {
Collection<Future> result = [1, 2, 3, 4, 5].collectParallel{it * 10}
assert new HashSet([10, 20, 30, 40, 50]) == new HashSet((Collection)result*.get())
}
//multiply numbers asynchronously using an asynchronous closure
GParsExecutorsPool.withPool {
def closure={it * 10}
def asyncClosure=closure.async()
Collection<Future> result = [1, 2, 3, 4, 5].collect(asyncClosure)
assert new HashSet([10, 20, 30, 40, 50]) == new HashSet((Collection)result*.get())
}
傳入的閉包會接收一個 ExecutorService 的實例作為參數,然後可以在閉包內自由使用。
1
2
3
4
//find an element meeting specified criteria
GParsExecutorsPool.withPool(5) {ExecutorService service ->
service.submit({performLongCalculation()} as Runnable)
}
GParsExecutorsPool.withPool() 方法接受一個可選參數,宣告所建立的池中的執行緒數量和一個執行緒工廠。
1
2
withPool(10) {...}
withPool(20, threadFactory) {...}
GParsExecutorsPool.withExistingPool() 接受一個已存在的 執行器服務實例
來重複使用。DSL 僅在相關程式碼區塊內且僅適用於呼叫 withPool() 或 withExistingPool() 方法的執行緒。
您是否知道 withExistingPool() 方法不會等待 執行器服務執行緒 完成? |
withPool() 方法僅在所有工作執行緒完成其任務且執行器服務已被銷毀後才會返回控制,並返回相關程式碼區塊的結果值。
靜態匯入 GParsExecutorsPool 類別,如 import static groovyx.gpars.GParsExecutorsPool.*,以省略 GParsExecutorsPool 類別名稱。 |
1
2
3
4
withPool {
def result = [1, 2, 3, 4, 5].findParallel{Number number -> number > 2}
assert result in [3, 4, 5]
}
以下方法目前在所有支援 Groovy 中迭代的物件上都支援
-
eachParallel()
-
eachWithIndexParallel()
-
collectParallel()
-
findAllParallel()
-
findParallel()
-
allParallel()
-
anyParallel()
-
grepParallel()
-
groupByParallel()
元類別增強器
作為替代方案,您可以使用 GParsExecutorsPoolEnhancer 類別來增強任何具有非同步方法之類別或個別實例的中繼類別。
1
2
3
4
5
6
7
8
9
10
11
import groovyx.gpars.GParsExecutorsPoolEnhancer
def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
GParsExecutorsPoolEnhancer.enhanceInstance(list)
println list.collectParallel {it * 2 }
def animals = ['dog', 'ant', 'cat', 'whale']
GParsExecutorsPoolEnhancer.enhanceInstance animals
println (animals.anyParallel {it ==~ /ant/} ? 'Found an ant' : 'No ants found')
println (animals.allParallel {it.contains('a')} ? 'All animals contain a' : 'Some animals can live without an a')
使用 GParsExecutorsPoolEnhancer 類別時,您不限於使用 GParsExecutorsPool DSL
的 withPool() 區塊。增強的類別或實例會保持增強狀態,直到它們被垃圾收集。
異常處理
在處理任何傳入的閉包時可能會擲回例外狀況。AsyncException 方法的實例將包裝從 xxxParallel 方法重新擲回的任何/所有原始例外狀況。
避免函式中的副作用
我們再次需要警告您關於使用具有副作用的閉包。請避免影響單個目前處理元素範圍之外的物件的邏輯。請避免保留狀態的邏輯或閉包。不要那樣做!將它們傳遞給任何 xxxParallel() 方法是很危險的。
Memoize (記憶化)
memoize 函數可以快取函數的傳回值。重複呼叫具有相同引數值的記憶化函數時,不是調用原始函數中編碼的計算,而是從內部透明快取中檢索結果值。
如果計算速度明顯慢於從快取中檢索快取值,開發人員可以使用記憶體來換取效能。
看看這個範例,我們嘗試掃描多個網站以查找特定內容
GPars 的 memoize 功能已捐贈給 Groovy 1.8 版,如果您在 Groovy 1.8 或更新版本上執行,我們建議您使用 Groovy 功能。
在 GPars 中,Memoize 幾乎相同,只是它使用周圍的執行緒池並行搜尋記憶化快取。這在某些情況下可能會帶來效能優勢。
使用範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GParsPool.withPool {
def urls = ['http://www.dzone.com', 'http://www.theserverside.com', 'http://www.infoq.com']
Closure download = {url ->
println "Downloading $url"
url.toURL().text.toUpperCase()
}
Closure cachingDownload = download.gmemoize()
println 'Groovy sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GROOVY')}
println 'Grails sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GRAILS')}
println 'Griffon sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GRIFFON')}
println 'Gradle sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GRADLE')}
println 'Concurrency sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('CONCURRENCY')}
println 'GPars sites today: ' + urls.findAllParallel {url -> cachingDownload(url).contains('GPARS')}
}
請注意,閉包如何在 GParsPool.withPool() 區塊內使用 memoize() 函數進行增強。這會傳回一個新的閉包,將原始閉包包裝為快取項目。
在先前的範例中,我們在程式碼中的多個位置呼叫了 cachingDownload 函數,但是,每個唯一的 URL 只會下載一次 - 第一次需要時。然後將這些值快取起來,並可供後續呼叫使用。此外,這些值也可供所有執行緒使用,無論哪個執行緒最初是針對該特定 URL 的下載請求而來並必須處理實際計算/下載。
因此,總而言之,memoize 呼叫會透過使用過去傳回值的快取來保護函數。
但是,memoize 可以做更多的事!在某些演算法中,添加少量記憶體可能會對計算的計算複雜度產生重大影響。讓我們看看 費波納契
數的經典範例。
費波那契數列範例
遵循費波納契數定義的純函數式遞迴實作具有指數複雜度
1
Closure fib = {n -> n > 1 ? call(n - 1) + call(n - 2) : n}
嘗試使用 30 左右的數字呼叫 fib 函數,您會看到它有多慢。
現在只需稍作修改並添加 memoize 快取,該演算法就會神奇地變成線性複雜度的演算法
1
2
Closure fib
fib = {n -> n > 1 ? fib(n - 1) + fib(n - 2) : n}.gmemoize()
我們添加的額外記憶體現在切斷了除一個遞迴分支之外的所有計算。而且,後續對同一個 fib 函數的所有呼叫也將受益於快取值。
請看以下內容,了解 memoizeAtMost 變體如何在我們的範例中減少記憶體消耗,但仍保留演算法的線性複雜度。
可用的變體
Memoize (記憶化)
基本變體會將值保留在記憶化函數的整個生命週期的內部快取中。它提供了所有變體中最佳的效能特性。
memoizeAtMost
允許我們對快取的項目數量設定硬性限制。一旦達到限制,所有後續新增的值都將使用 LRU (最近最少使用
) 策略從快取中消除最舊的值。
因此,對於我們的費波納契數範例,我們可以安全地將快取大小減少到兩個項目
1
2
Closure fib
fib = {n -> n > 1 ? fib(n - 1) + fib(n - 2) : n}.memoizeAtMost(2)
設定快取大小的上限有兩個目的
-
將快取的記憶體佔用量保持在定義的界限內
-
保留函數所需的效能特性。與直接計算結果所需的時間相比,過大的快取會增加檢索快取值的時間。
memoizeAtLeast
允許內部快取無限增長,直到 JVM 的垃圾收集器決定介入並從記憶體中逐出 SoftReferences
項目(我們的實作所使用)。
memoizeAtLeast() 方法的單一參數表示應保護免於垃圾收集逐出的最小快取項目數。快取永遠不會縮小到指定的項目數以下。快取會確保僅使用 LRU (最近最少使用
) 策略保護最近使用的項目免於逐出。
memoizeBetween
結合 memoizeAtLeast 和 memoizeAtMost 方法,以允許快取在兩個參數值之間的範圍內增長和縮小,具體取決於可用記憶體和垃圾收集活動。
快取大小永遠不會超過上限,以保留所需的快取效能特性。
Map-Reduce (映射歸約)
平行集合 Map/Reduce
DSL 為 GPars 賦予了更具函數式的風格。一般而言,Map/Reduce DSL
可用於與 xxxParallel() 系列方法相同的目的,並且具有非常相似的語義。另一方面,如果您需要將多個方法鏈接在一起以多個步驟處理單個集合,則 Map/Reduce 的執行速度可能會快得多
1
2
3
4
5
6
println 'Number of occurrences of the word GROOVY today: ' + urls.parallel
.map {it.toURL().text.toUpperCase()}
.filter {it.contains('GROOVY')}
.map{it.split()}
.map{it.findAll{word -> word.contains 'GROOVY'}.size()}
.sum()
xxxParallel() 方法必須遵循與其非並行對等方法相同的合約。因此,collectParallel() 方法必須傳回合法的項目集合,您可以將其視為 Groovy 集合。
在內部,平行收集方法 會建立一個稱為 平行陣列
的高效平行結構。然後它會同時執行所需的運算。在返回之前,它會銷毀 平行陣列,因為它會建構一個結果集合傳回給您。例如,對結果集合呼叫 findAllParallel() 可能會重複整個建構和銷毀 平行陣列
實例的過程。
使用 Map/Reduce,您只會將您的集合轉換為 平行陣列
並再次轉換一次。Map/Reduce 系列方法不會傳回 Groovy 集合,但可以自由地直接傳遞內部 平行陣列
。
調用集合的 parallel 屬性將會為集合建構 平行陣列
,然後傳回 平行陣列
實例周圍的輕量級包裝器。然後,您可以將任何這些方法鏈接在一起以獲得答案
-
map()
-
reduce()
-
filter()
-
size()
-
sum()
-
min()
-
max()
-
sort()
-
groupBy()
-
combine()
傳回普通的 Groovy 集合實例始終只是檢索 collection 屬性的問題。
1
def myNumbers = (1..1000).parallel.filter{it % 2 == 0}.map{Math.sqrt it}.collection
避免函式中的副作用
我們再次需要警告您。為了避免令人不快的意外,請保持您傳遞給 Map/Reduce 函數的任何閉包都是無狀態且沒有副作用的。
為了避免令人不快的意外,請保持您的閉包無狀態 |
可用性
此功能僅在使用基於 Fork/Join 的 GParsPool 時可用,而不能在 GParsExecutorsPool 方法中使用。
經典範例
一個受 thevery 啟發的經典範例,用於計算字串中單字的出現次數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import static groovyx.gpars.GParsPool.withPool
def words = "This is just plain text to count words in"
print count(words)
def count(arg) {
withPool {
return arg.parallel
.map{[it, 1]}
.groupBy{it[0]}.getParallel()
.map {it.value=it.value.size();it}
.sort{-it.value}.collection
}
}
可以使用更通用的 combine 運算實作相同的範例
1
2
3
4
5
6
7
8
9
10
11
12
13
def words = "This is just plain text to count words in"
print count(words)
def count(arg) {
withPool {
return arg.parallel
.map{[it, 1]}
.combine(0) {sum, value -> sum + value}.getParallel()
.sort{-it.value}.collection
}
}
Combine (合併)
combine 運算預期一個由元組(雙元素列表)組成的輸入列表,通常被認為是鍵值對(例如 [ [key1, value1], [key2, value2], [key1, value3], [key3, value4] … ] )。這些可能具有潛在的重複鍵。
調用時,combine 方法會使用提供的累積函數合併具有相同鍵的值。這會產生一個包含原始(唯一)鍵及其(現在)累積值的對應表。
累積函數引數需要指定一個函數,以在組合(累積)屬於同一個鍵的值時使用。還需要提供一個初始累積值。
由於 combine 方法並行處理項目,因此 初始累積值 將被多次重複使用。因此,提供的值必須允許重複使用。
它應該是一個 可複製(或 不可變)的值,或是一個 閉包,每次請求時都會傳回一個新的初始累積器。累積函數和可重複使用的初始值的良好組合包括
1
2
3
4
5
accumulator = {List acc, value -> acc << value} initialValue = []
accumulator = {List acc, value -> acc << value} initialValue = {-> []}
accumulator = {int sum, int value -> acc + value} initialValue = 0
accumulator = {int sum, int value -> sum + value} initialValue = {-> 0}
accumulator = {ShoppingCart cart, Item value -> cart.addItem(value)} initialValue = {-> new ShoppingCart()}
回傳類型為一個 Map。 |
例如,[['he', 1], ['she', 2], ['he', 2], ['me', 1], ['she', 5], ['he', 1]],初始值為零,將會合併為 ['he' : 4, 'she' : 7, 'me' : 1]
對於較複雜的情況,當您 combine() 複雜物件時,一個好的策略是使用一個完整的類別作為常用情況的鍵值,並為不常見的情況應用不同的鍵值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import groovy.transform.ToString
import groovy.transform.TupleConstructor
import static groovyx.gpars.GParsPool.withPool
// declare a complete class to use in combination processing
@TupleConstructor @ToString
class PricedCar implements Cloneable { // either Clonable or Immutable
String model
String color
Double price
// declare a way to resolve comparison logic
boolean equals(final o) {
if (this.is(o)) return true
if (getClass() != o.class) return false
final PricedCar pricedCar = (PricedCar) o
if (color != pricedCar.color) return false
if (model != pricedCar.model) return false
return true
}
int hashCode() {
int result
result = (model != null ? model.hashCode() : 0)
result = 31 * result + (color != null ? color.hashCode() : 0)
return result
}
@Override
protected Object clone() {
return super.clone()
}
}
// some data
def cars = [new PricedCar('F550', 'blue', 2342.223),
new PricedCar('F550', 'red', 234.234),
new PricedCar('Da', 'white', 2222.2),
new PricedCar('Da', 'white', 1111.1)]
withPool {
//Combine by model
def result =
cars.parallel.map {
[it.model, it]
}.combine(new PricedCar('', 'N/A', 0.0)) {sum, value ->
sum.model = value.model
sum.price += value.price
sum
}.values()
println result
//Combine by model and color (using the PricedCar's equals and hashCode))
result =
cars.parallel.map {
[it, it]
}.combine(new PricedCar('', 'N/A', 0.0)) {sum, value ->
sum.model = value.model
sum.color = value.color
sum.price += value.price
sum
}.values()
println result
}
平行陣列
作為替代方案,可以直接使用 JSR-166y - Java 並行處理 中定義的有效率的樹狀資料結構。任何集合或物件的 parallelArray 屬性都會回傳一個 ParallelArray 實例,其中包含原始集合的元素。然後可以透過 jsr166y API 來操作這些實例。
請參閱 jsr166y 文件以了解 API 詳細資訊。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import groovyx.gpars.extra166y.Ops
groovyx.gpars.GParsPool.withPool {
assert 15 == [1, 2, 3, 4, 5].parallelArray.reduce({a, b -> a + b} as Ops.Reducer, 0) //summarize
assert 55 == [1, 2, 3, 4, 5].parallelArray.withMapping({it ** 2} as Ops.Op).reduce({a, b -> a + b} as Ops.Reducer, 0) //summarize squares
assert 20 == [1, 2, 3, 4, 5].parallelArray.withFilter({it % 2 == 0} as Ops.Predicate) //summarize squares of even numbers
.withMapping({it ** 2} as Ops.Op)
.reduce({a, b -> a + b} as Ops.Reducer, 0)
assert 'aa:bb:cc:dd:ee' == 'abcde'.parallelArray //concatenate duplicated characters with separator
.withMapping({it * 2} as Ops.Op)
.reduce({a, b -> "$a:$b"} as Ops.Reducer, "")
非同步調用
在大多數系統中,長時間運行的背景任務很常見。
通常,主執行緒會想要初始化一些計算、開始下載、執行搜尋等等,即使可能不需要立即取得結果。
GPars 為開發人員提供了工具,可以排程非同步活動進行背景處理,並在需要時稍後收集結果。
使用 GParsPool 和 GParsExecutorsPool 非同步處理功能
GParsPool 和 GParsExecutorsPool 方法都提供幾乎相同的服務,同時利用不同的底層機制。
閉包增強功能
以下方法會加入到 GPars(Executors)Pool.withPool() 區塊內的閉包中
-
async() - 建立提供的閉包的非同步變體,當呼叫時,會回傳一個 future 物件,代表潛在的回傳值
-
callAsync() - 在獨立的執行緒中呼叫閉包,同時提供指定的引數,回傳一個 future 物件,代表潛在的回傳值
1
2
3
4
5
6
7
8
9
GParsPool.withPool() {
Closure longLastingCalculation = {calculate()}
Closure fastCalculation = longLastingCalculation.async() //create a new closure, which starts the original closure on a thread pool
Future result=fastCalculation() //returns almost immediately
//do stuff while calculation performs ...
println result.get()
}
1
2
3
4
5
6
7
8
GParsPool.withPool() {
/**
* The callAsync() method is an asynchronous variant of the default call() method to invoke a closure.
* It will return a Future for the result value.
*/
assert 6 == {it * 2}.call(3)
assert 6 == {it * 2}.callAsync(3).get()
}
逾時
callTimeoutAsync() 方法,接受 long 值或 Duration 實例,提供計時器機制。
1
2
3
4
5
6
{->
while(true) {
Thread.sleep 1000 //Simulate a bit of interesting calculation
if (Thread.currentThread().isInterrupted()) break; //We've been cancelled
}
}.callTimeoutAsync(2000)
為了允許取消,我們的非同步執行程式碼必須持續檢查它自己執行緒的 interrupted 旗標,並在旗標設為 true 時停止計算。
執行器服務增強功能
ExecutorService 和 ForkJoinPool 類別都透過 '<<' (leftShift) 運算子進行增強,以將任務提交到池中並回傳結果的 Future。
1
2
3
GParsExecutorsPool.withPool {ExecutorService executorService ->
executorService << {println 'Inside parallel task'}
}
平行執行函式(閉包)
GParsPool 和 GParsExecutorsPool 類別也提供方便的方法 executeAsync() 和 executeAsyncAndWait(),可輕鬆地非同步執行多個閉包。
範例
1
2
3
4
GParsPool.withPool {
assert [10, 20] == GParsPool.executeAsyncAndWait({calculateA()}, {calculateB()} //waits for results
assert [10, 20] == GParsPool.executeAsync({calculateA()}, {calculateB()})*.get() //returns Futures instead and doesn't wait for results to be calculated
}
可組合的非同步函式
函式應該被組合。事實上,組合無副作用的函式非常容易。例如,比組合物件更容易也更可靠。
在給定相同輸入的情況下,函式始終會回傳相同的結果,它們永遠不會意外地變更其行為,也不會在多個執行緒同時呼叫它們時發生錯誤。
Groovy 中的函式
我們可以將 Groovy 閉包視為函式。它們接收引數、執行計算並回傳值。只要您不讓閉包接觸其範圍之外的任何內容,您的閉包就會表現良好,就像純函式一樣。您可以組合這些函式以實現更高的目標。
1
def sum = (0..100000).inject(0, {a, b -> a + b})
在這個範例中,透過將加總兩個數字的函式 {a,b}
與 inject 函式結合,後者會迭代整個集合,您可以快速彙總所有項目。然後,將 adding 函式替換為 comparison 函式,立即會得到一個組合函式來計算最大值。
1
def max = myNumbers.inject(0, {a, b -> a>b?a:b})
您會發現,函數式程式設計受歡迎是有原因的。
我們實現並行了嗎?
這一切運作良好,直到您發現自己沒有充分利用昂貴硬體的全部功能。這些函式只是簡單的循序執行!沒有使用平行處理!除了其中一個處理器核心外,其他核心什麼都沒做,它們處於閒置狀態,完全被浪費了!
除了其中一個處理器核心外,其他核心什麼都沒做!它們處於閒置狀態!完全被浪費了! |
為了讓事情更清楚,這裡有一個組合四個函式的範例,這些函式應該檢查特定網頁是否與本機檔案的內容相符。我們需要下載頁面、載入檔案、計算兩者的雜湊,最後比較產生的數字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Closure download = {String url ->
url.toURL().text
}
Closure loadFile = {String fileName ->
... //load the file here
}
Closure hash = {s -> s.hashCode()}
Closure compare = {int first, int second ->
first == second
}
def result = compare(hash(download('http://www.gpars.org')), hash(loadFile('/coolStuff/gpars/website/index.html')))
println "The result of comparison: " + result
我們需要下載頁面、載入檔案、計算兩者的雜湊,最後比較產生的數字。每個函式都負責一項特定的工作。一個函式下載內容,第二個函式載入檔案,第三個函式計算雜湊,最後第四個函式執行比較。
組合函式就像巢狀呼叫一樣簡單。
讓一切都成為非同步
我們程式碼的缺點是,我們沒有利用 download() 和 loadFile() 函式的獨立性。我們也沒有讓兩個雜湊同時執行。它們可以很好地平行執行,但我們組合函式的方法限制了平行處理。
顯然,並非所有函式都 可以 同時執行。有些函式取決於其他函式的結果。它們在其他函式完成之前無法開始。我們需要阻止它們,直到它們的參數可用為止。hash() 函式需要一個字串才能運作。compare() 函式需要兩個數字進行比較。
因此,我們只能在一定程度上進行平行處理,同時阻止其他函式的平行處理。這似乎是一項具有挑戰性的任務。
在函數式世界中,一切都很美好
幸運的是,函式之間的依賴關係已經隱式地在程式碼中表示出來。不需要重複這些依賴關係資訊。如果一個函式採用參數,而參數需要先由另一個函式計算,我們在這裡就隱式地有了依賴關係。
在我們的範例中,hash() 函式依賴於 loadFile() 以及 download() 函式。我們較早的範例中的 inject 函式依賴於在集合的所有元素上逐漸呼叫的 addition 函式的結果。
在 GPars 的最佳傳統中,我們讓您非常容易地說服任何函式相信其他函式的 promises。在閉包上呼叫 asyncFun() 函式,您就實現非同步了!
1
2
3
4
5
6
withPool {
def maxPromise = numbers.inject(0, {a, b -> a>b?a:b}.asyncFun())
println "Look Ma, I can talk to the user while the math is being done for me!"
println maxPromise.get()
}
inject 函式並不在意 addition 函式回傳什麼物件,也許它會對每次呼叫 addition 函式都如此快速感到有點驚訝,但它不會抱怨太多,它會繼續迭代,最後回傳我們預期的整體結果。
現在是您應該為自己的話負責並做您希望別人做的事情的時候了。不要對結果皺眉,並接受您只收到一個 promise。一個 promise,保證在計算完成後立即交付答案。您的筆記型電腦發出的額外熱量表明,計算利用了函式中的自然平行處理,並盡最大努力快速向您交付結果。
1
2
3
4
5
6
7
withPool {
def sumPromise = (0..100000).inject(0, {a, b -> a + b}.asyncFun())
println "Are we done yet? " + sumPromise.bound
sumPromise.whenBound {sum -> println sum}
}
事情會出錯嗎?
當然。但是您會從 promise get() 方法中收到拋出的例外狀況。
1
2
3
4
5
6
try {
sumPromise.get()
} catch (MyCalculationException e) {
println "Guess, things are not ideal today."
}
這一切都很好,但是哪些函式真的可以組合?
您的野心沒有限制。採用您需要組合的任何循序函式,您應該也可以組合它們的非同步變體。
回顧我們最初的範例,將檔案的內容與網頁進行比較。我們只需呼叫 asyncFun() 方法使所有函式都成為非同步,我們就可以開始了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Closure download = {String url ->
url.toURL().text
}.asyncFun()
Closure loadFile = {String fileName ->
... //load the file here
}.asyncFun()
Closure hash = {s -> s.hashCode()}.asyncFun()
Closure compare = {int first, int second ->
first == second
}.asyncFun()
def result = compare(hash(download('http://www.gpars.org')), hash(loadFile('/coolStuff/gpars/website/index.html')))
println 'Allowed to do something else now'
println "The result of comparison: " + result.get()
從非同步函式中呼叫非同步函式
非同步函式的另一個非常有價值的屬性是,可以組合 promises
。
可以組合 Promises! |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import static groovyx.gpars.GParsPool.withPool
withPool {
Closure plus = {Integer a, Integer b ->
sleep 3000
println 'Adding numbers'
a + b
}.asyncFun(); // ok, here's one func
Closure multiply = {Integer a, Integer b ->
sleep 2000
a * b
}.asyncFun() // and second one
Closure measureTime = {->
sleep 3000
4
}.asyncFun(); // and another
// declare a function within a function
Closure distance = {Integer initialDistance, Integer velocity, Integer time ->
plus(initialDistance, multiply(velocity, time))
}.asyncFun(); // and another
Closure chattyDistance = {Integer initialDistance, Integer velocity, Integer time ->
println 'All parameters are now ready - starting'
println 'About to call another asynchronous function'
def innerResultPromise = plus(initialDistance, multiply(velocity, time))
println 'Returning the promise for the inner calculation as my own result'
return innerResultPromise
}.asyncFun(); // and declare (but not run) a final asynch.function
// fine, now let's execute those previous asynch. functions
println "Distance = " + distance(100, 20, measureTime()).get() + ' m'
println "ChattyDistance = " + chattyDistance(100, 20, measureTime()).get() + ' m'
}
如果非同步函式(例如,此範例中的 distance 函式)在其主體中呼叫另一個非同步函式(例如,plus),並回傳被呼叫函式的 promise,則內部函式(plus)的結果 promise 將與外部函式(distance)的結果 promise 結合。
一旦內部函式 (plus) 完成其計算,內部函式 (plus) 現在會將其結果繫結到外部函式 (distance) 的 promise。這種 promise 組合邏輯的能力,可讓函式在不封鎖執行緒的情況下停止計算。這不僅在等待參數時發生,而且在它們於程式碼主體中的任何位置呼叫另一個非同步函式時也會發生。
方法作為非同步函式
可以使用 .& 運算子將方法視為閉包來參照。然後,可以使用 asyncFun 方法將這些閉包轉換為可組合的非同步函式,就像普通的閉包一樣。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class DownloadHelper {
String download(String url) {
url.toURL().text
}
int scanFor(String word, String text) {
text.findAll(word).size()
}
String lower(s) {
s.toLowerCase()
}
}
//now we'll make the methods asynchronous
withPool {
final DownloadHelper d = new DownloadHelper()
Closure download = d.&download.asyncFun() // notice the .& syntax
Closure scanFor = d.&scanFor.asyncFun() // and here
Closure lower = d.&lower.asyncFun() // and here
//asynchronous processing
def result = scanFor('groovy', lower(download('http://www.infoq.com')))
println 'Doing something else for now'
println result.get()
}
使用註解建立非同步函式
可以使用 @AsyncFun 註解來註解閉包類型的欄位,而不是呼叫 asyncFun() 函式。欄位必須就地初始化,而且包含的類別需要在 withPool 區塊內實例化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import static groovyx.gpars.GParsPool.withPool
import groovyx.gpars.AsyncFun
class DownloadingSearch {
@AsyncFun Closure download = {String url ->
url.toURL().text
}
@AsyncFun Closure scanFor = {String word, String text ->
text.findAll(word).size()
}
@AsyncFun Closure lower = {s -> s.toLowerCase()}
void scan() {
def result = scanFor('groovy', lower(download('http://www.infoq.com'))) //synchronous processing
println 'Allowed to do something else now'
println result.get()
}
}
withPool {
new DownloadingSearch().scan()
}
替代池
預設情況下,AsyncFun 註解會使用包裝 withPool
區塊中的 GParsPool 實例。但是,您可以明確指定池的類型
1
@AsyncFun(GParsExecutorsPoolUtil) def sum6 = {a, b -> a + b }
透過註解封鎖函式
AsyncFun 方法也允許我們指定產生的函式是否應允許封鎖 (true) 或非封鎖 (false - 預設) 語意。
1
2
@AsyncFun(blocking = true)
def sum = {a, b -> a + b }
明確且延遲的池指派
當直接使用 GPars(Executors)PoolUtil.asyncFun() 函數建立非同步函數時,您有兩種額外的方式可以將執行緒池指派給該函數。
-
該函數要使用的執行緒池可以在建立時明確地指定為額外的參數。
-
隱含的執行緒池可以在調用時從周圍的作用域取得,而不是在建立時取得。
當明確指定執行緒池時,呼叫不需要包在 withPool() 區塊中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Closure sPlus = {Integer a, Integer b ->
a + b
}
Closure sMultiply = {Integer a, Integer b ->
sleep 2000
a * b
}
println "Synchronous result: " + sMultiply(sPlus(10, 30), 100)
final pool = new FJPool();
Closure aPlus = GParsPoolUtil.asyncFun(sPlus, pool)
Closure aMultiply = GParsPoolUtil.asyncFun(sMultiply, pool)
def result = aMultiply(aPlus(10, 30), 100)
println "Time to do something else while the calculation is running"
println "Asynchronous result: " + result.get()
使用延遲的池指派,只有函數調用必須用 withPool() 區塊包住。
1
2
3
4
5
6
7
8
9
Closure aPlus = GParsPoolUtil.asyncFun(sPlus)
Closure aMultiply = GParsPoolUtil.asyncFun(sMultiply)
withPool {
def result = aMultiply(aPlus(10, 30), 100)
println "Time to do something else while the calculation is running"
println "Asynchronous result: " + result.get()
}
對我們而言,這是一個非常值得探索的領域。因此,歡迎對結合非同步函數提出任何意見、問題或建議,或是有關於其限制的提示。
Fork-Join (分支合併)
Fork/Join 或分而治之,是一種非常強大的抽象概念,可以用來解決階層式問題。
抽象概念
當談到階層式問題時,請思考快速排序、合併排序、檔案系統或一般樹狀結構導覽問題。
-
Fork/Join 演算法本質上是將問題分割成幾個較小的子問題,然後遞迴地將相同的演算法應用於每個子問題。
-
一旦子問題夠小,就會直接解決。
-
所有子問題的解決方案會組合起來以解決它們的父問題,進而幫助解決其本身的祖父問題。
強大的 JSR-166y 函式庫可以很好地協調 Fork/Join 的編排,但留下了一些粗糙的邊緣,如果您不夠注意,可能會傷害您。您仍然必須處理執行緒、池和/或同步屏障。
GPars 抽象便利層
GPars 可以為您隱藏處理執行緒、池和遞迴任務的複雜性,同時讓您利用 jsr166y 中強大的 Fork/Join 實作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import static groovyx.gpars.GParsPool.runForkJoin
import static groovyx.gpars.GParsPool.withPool
withPool() {
println """Number of files: ${
runForkJoin(new File("./src")) {file ->
long count = 0
file.eachFile {
if (it.isDirectory()) {
println "Forking a child task for $it"
forkOffChild(it) //fork a child task
} else {
count++
}
}
return count + (childrenResults.sum(0))
//use results of children tasks to calculate and store own result
}
}""".toString();
}
runForkJoin() 工廠方法使用提供的遞迴程式碼以及提供的值來建立階層式的 Fork/Join 計算。傳遞給 runForkJoin() 方法的值的數量必須與閉包預期的參數數量相符。這必須等於傳遞給 forkOffChild() 或 runChildDirectly() 方法的引數數量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def quicksort(numbers) {
withPool {
runForkJoin(0, numbers) {index, list ->
def groups = list.groupBy {it <=> list[list.size().intdiv(2)]}
if ((list.size() < 2) || (groups.size() == 1)) {
return [index: index, list: list.clone()]
}
(-1..1).each {forkOffChild(it, groups[it] ?: [])}
return [index: index, list: childrenResults.sort {it.index}.sum {it.list}]
}.list
}
}
替代方法
或者,可以直接使用巢狀 Fork/Join 工作任務的底層機制。客製化的工作者可以消除使用通用工作者時,由於參數擴散而產生的效能開銷。
此外,客製化的工作者可以用 Java 實作,以進一步提高效能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class FileCounter extends AbstractForkJoinWorker<Long> {
private final File file;
def FileCounter(final File file) {
this.file = file
}
@Override
protected Long computeTask() {
long count = 0;
file.eachFile {
if (it.isDirectory()) {
println "Forking a thread for $it"
forkOffChild(new FileCounter(it)) //fork a child task
} else {
count++
}
}
return count + ((childrenResults)?.sum() ?: 0) //use results of children tasks to calculate and store own result
}
}
withPool(1) {pool -> //feel free to experiment with the number of fork/join threads in the pool
println "Number of files: ${runForkJoin(new FileCounter(new File("..")))}"
}
AbstractForkJoinWorker 子類別可以用 Java 和 Groovy 撰寫。如果工作者的效能低下成為瓶頸,兩種選擇都可以讓您針對執行速度進行最佳化。
Fork / Join 節省您的資源
由於內部使用 TaskBarrier 類別來同步執行緒,因此 Fork/Join 操作可以安全地以少數執行緒執行。
當執行緒在演算法內被阻塞,等待其子問題被計算時,該執行緒會默默地返回其池,以從任務佇列中取得任何其他可用的子問題並加以處理。儘管該演算法建立的任務數量與子目錄數量相同,並且任務等待子目錄任務完成,但通常只需要單一執行緒就足以讓計算持續進行,並最終計算出有效結果。
合併排序範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import static groovyx.gpars.GParsPool.runForkJoin
import static groovyx.gpars.GParsPool.withPool
/**
* Splits a list of numbers in half
*/
def split(List<Integer> list) {
int listSize = list.size()
int middleIndex = listSize / 2
def list1 = list[0..<middleIndex]
def list2 = list[middleIndex..listSize - 1]
return [list1, list2]
}
/**
* Merges two sorted lists into one
*/
List<Integer> merge(List<Integer> a, List<Integer> b) {
int i = 0, j = 0
final int newSize = a.size() + b.size()
List<Integer> result = new ArrayList<Integer>(newSize)
while ((i < a.size()) && (j < b.size())) {
if (a[i] <= b[j]) result << a[i++]
else result << b[j++]
}
if (i < a.size()) result.addAll(a[i..-1])
else result.addAll(b[j..-1])
return result
}
final def numbers = [1, 5, 2, 4, 3, 8, 6, 7, 3, 4, 5, 2, 2, 9, 8, 7, 6, 7, 8, 1, 4, 1, 7, 5, 8, 2, 3, 9, 5, 7, 4, 3]
withPool(3) { //feel free to experiment with the number of fork/join threads in the pool
println """Sorted numbers: ${
runForkJoin(numbers) {nums ->
println "Thread ${Thread.currentThread().name[-1]}: Sorting $nums"
switch (nums.size()) {
case 0..1:
return nums //store own result
case 2:
if (nums[0] <= nums[1]) return nums //store own result
else return nums[-1..0] //store own result
default:
def splitList = split(nums)
[splitList[0], splitList[1]].each {forkOffChild it} //fork a child task
return merge(* childrenResults) //use results of children tasks to calculate and store own result
}
}
}"""
}
使用客製化工作者類別的合併排序範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public final class SortWorker extends AbstractForkJoinWorker<List<Integer>> {
private final List numbers
def SortWorker(final List<Integer> numbers) {
this.numbers = numbers.asImmutable()
}
/**
* Splits a list of numbers in half
*/
def split(List<Integer> list) {
int listSize = list.size()
int middleIndex = listSize / 2
def list1 = list[0..<middleIndex]
def list2 = list[middleIndex..listSize - 1]
return [list1, list2]
}
/**
* Merges two sorted lists into one
*/
List<Integer> merge(List<Integer> a, List<Integer> b) {
int i = 0, j = 0
final int newSize = a.size() + b.size()
List<Integer> result = new ArrayList<Integer>(newSize)
while ((i < a.size()) && (j < b.size())) {
if (a[i] <= b[j]) result << a[i++]
else result << b[j++]
}
if (i < a.size()) result.addAll(a[i..-1])
else result.addAll(b[j..-1])
return result
}
/**
* Sorts a small list or delegates to two children, if the list contains more than two elements.
*/
@Override
protected List<Integer> computeTask() {
println "Thread ${Thread.currentThread().name[-1]}: Sorting $numbers"
switch (numbers.size()) {
case 0..1:
return numbers //store own result
case 2:
if (numbers[0] <= numbers[1]) return numbers //store own result
else return numbers[-1..0] //store own result
default:
def splitList = split(numbers)
[new SortWorker(splitList[0]), new SortWorker(splitList[1])].each{forkOffChild it} //fork a child task
return merge(* childrenResults) //use results of children tasks to calculate and store own result
}
}
}
final def numbers = [1, 5, 2, 4, 3, 8, 6, 7, 3, 4, 5, 2, 2, 9, 8, 7, 6, 7, 8, 1, 4, 1, 7, 5, 8, 2, 3, 9, 5, 7, 4, 3]
withPool(1) { //feel free to experiment with the number of fork/join threads in the pool
println "Sorted numbers: ${runForkJoin(new SortWorker(numbers))}"
}
直接執行子任務
forkOffChild 方法有一個兄弟方法——稱為 runChildDirectly 方法。此方法將在目前執行緒內直接且立即執行子任務,而不是排程子任務在執行緒池上進行非同步處理。通常,您會在每個子任務(但最後一個除外)上呼叫 forkOffChild,而最後一個子任務會直接調用,而無需排程開銷。
1
2
3
4
5
6
7
8
9
10
11
12
13
Closure fib = {number ->
if (number <= 2) {
return 1
}
forkOffChild(number - 1) // This task will run asynchronously, probably in a different thread
final def result = runChildDirectly(number - 2) // This task is run directly within the current thread
return (Integer) getChildrenResults().sum() + result
}
withPool {
assert 55 == runForkJoin(10, fib)
}
可用性
此功能僅在使用基於 Fork/Join 的 GParsPool 時可用,但 GParsExecutorsPool 不可用。
平行推測
隨著處理器核心變得充足,某些演算法可能會受益於暴力平行複製。您不是預先決定如何解決問題、使用哪個演算法或要連接到哪個位置,而是平行執行所有可能的解決方案。
平行推測
想像一下,您需要執行一項任務,例如計算昂貴的函數或從檔案、資料庫或網際網路讀取資料。幸運的是,您知道有幾種好方法(例如函數或 URL)可以達成您的目標。但是,並非所有方法都相同。
儘管它們返回相同的結果(就您的需求而言),但每個方法的經過時間都會有所不同,有些方法甚至可能會失敗(例如,網路問題)。更糟糕的是,沒有人會告訴您哪個選擇能提供最佳解決方案,也沒有人會告訴您哪些路徑可能會導致無解。
-
我應該在我的清單上執行快速排序還是合併排序?
-
哪個 URL 的效果最好?
-
此服務在其主要位置是否可用,還是我應該使用備份位置?
GPars Speculations 讓您可以選擇平行嘗試所有可用的替代方案,並從最快的可用路徑接收結果,同時默默地忽略慢速或損壞的路徑。
這是 GParsPool 和 GParsExecutorsPool 上的 speculate 方法可以為您做的事情。
1
2
3
4
def numbers = ...
def quickSort = ...
def mergeSort = ...
def sortedNumbers = speculate(quickSort, mergeSort)
因此,我們同時(並行)執行快速排序和合併排序,同時取得速度較快的結果。
鑑於現今主流硬體上可用的平行資源,並行執行這兩個函數不會對其中任何一個函數的計算速度產生巨大影響,因此我們取得這兩者的結果所花費的時間,與我們只執行兩者中較快的計算所花費的時間大致相同。而且,結果的到達時間也比執行較慢的結果還要早。然而,我們不必預先知道哪種排序演算法在我們的資料上表現會更好。因此我們推測(猜測)。
同樣地,從多個速度和/或可靠性不同的來源下載文件可能如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import static groovyx.gpars.GParsPool.speculate
import static groovyx.gpars.GParsPool.withPool
def alternative1 = {
'http://www.dzone.com/links/index.html'.toURL().text
}
def alternative2 = {
'http://www.dzone.com/'.toURL().text
}
def alternative3 = {
'http://www.dzzzzzone.com/'.toURL().text //wrong url
}
def alternative4 = {
'http://dzone.com/'.toURL().text
}
withPool(4){
println speculate([alternative1, alternative2, alternative3, alternative4]).contains('groovy')
}
使用 Dataflow Variables 和 Streams 的替代方案
在某些用例中,我們可以忽略失敗的替代方案,因此可以使用 Dataflow 變數或 Streams 來取得獲勝推測的結果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.task
def alternative1 = {
'http://www.dzone.com/links/index.html'.toURL().text
}
def alternative2 = {
'http://www.dzone.com/'.toURL().text
}
def alternative3 = {
'http://www.dzzzzzone.com/'.toURL().text //will fail due to wrong url
}
def alternative4 = {
'http://dzone.com/'.toURL().text
}
//Pick either one of the following, both will work:
final def result = new DataflowQueue()
// final def result = new DataflowVariable()
[alternative1, alternative2, alternative3, alternative4].each{code ->
task{
try {
result << code()
}
catch (ignore) { } // We deliberately ignore unsuccessful urls.
}
}
println result.val.contains('groovy')

CSP 使用者指南
通訊循序處理程序
CSP(Communicating Sequential Processes,循序處理程序通訊)抽象概念建立在獨立的可組合程序之上,這些程序以同步方式交換訊息。GPars 利用英國肯特大學開發的 JCSP 函式庫。
GPars 中 CSP 實作的作者 Jon Kerridge 在 www.soc.napier.ac.uk 或 我們本地鏡像頁面的這裡提供了關於 GroovyCSP 使用的詳盡範例。
如果 LGPL 授權不適合您的使用,您可以考慮查看本 User Guide 的 Dataflow Concurrency 章節,以了解 tasks、selectors 和 operators,這些內容可能可以幫助您以類似於 CSP 方法的方式解決並行問題。事實上,在 GPars 中實作的資料流和 CSP 概念非常接近。
CSP 模型原則
本質上,CSP 模型建立在獨立的並行程序之上,這些程序透過通道使用同步(即會合)訊息傳遞來相互通訊。與圍繞事件處理模式的 actors 或資料流運算子不同,CSP 程序將其活動(又稱步驟序列)的重點放在使用通訊來沿途保持彼此同步。
由於尋址是透過通道間接進行的,因此程序不需要彼此了解。它們通常由一組輸入和輸出通道以及一個主體組成。一旦 CSP 程序啟動,它會從執行緒池中取得一個執行緒,並開始處理其主體,僅在從通道讀取或寫入通道時暫停。某些實作(例如 GoLang)也可以在通道被阻塞時,將執行緒從 CSP 程序分離。
CSP 程式是決定性的。程式輸入上的相同資料始終會產生相同的輸出,而與實際使用的執行緒排程方案無關。這在除錯 CSP 程式以及分析死鎖時有很大的幫助。
決定性與間接尋址相結合,使得 CSP 程序具有很高的可組合性。您只需連接它們的輸入和輸出通道,然後將它們包在另一個更大的包含程序中,就可以將小的 CSP 程序組合成更大的程序。
CSP 模型使用替代方案引入了不確定性。程序可以嘗試透過稱為替代方案或選擇的建構,同時從多個通道讀取值。在 Select 中涉及的任何通道中第一個可用的值,將會被程序讀取和消耗。由於透過 Select 收到的訊息順序取決於程式執行期間不可預測的條件,因此將讀取的值是不確定的。
使用 GPars 資料流的 CSP
GPars 提供了建立 CSP 程序的所有必要建構區塊。
-
可以使用 Closure、Runnable 或 Callable 來建立 GPars 任務的 CSP 程序模型,以保留程序的實際實作。
-
CSP 通道 應使用 SyncDataflowQueue 和 SyncDataflowBroadcast 類別來建模
-
CSP 替代方案透過 Select 類別及其 select 和 prioritySelect 方法提供。
處理程序
要啟動程序,只需使用 task 工廠方法即可。
1
2
3
4
5
6
7
8
9
10
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool
group = new DefaultPGroup(new ResizeablePool(true))
def t = group.task {
println "I am a process"
}
t.join()
由於每個程序在其生命週期內都會消耗一個執行緒,因此建議使用如上述範例中的可調整大小的執行緒池。 |
程序也可以從 Runnable 或 Callable 物件建立
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool
group = new DefaultPGroup(new ResizeablePool(true))
class MyProcess implements Runnable {
@Override
void run() {
println "I am a process"
}
}
def t = group.task new MyProcess()
t.join()
使用 Callable 可以透過 get() 方法回傳數值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool
import java.util.concurrent.Callable
group = new DefaultPGroup(new ResizeablePool(true))
class MyProcess implements Callable<String> {
@Override
String call() {
println "I am a process"
return "CSP is great!"
}
}
def t = group.task new MyProcess()
println t.get()
通道
程序通常需要通道與其伴隨程序以及外部世界進行溝通。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import groovy.transform.TupleConstructor
import groovyx.gpars.dataflow.DataflowReadChannel
import groovyx.gpars.dataflow.DataflowWriteChannel
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool
import java.util.concurrent.Callable
import groovyx.gpars.dataflow.SyncDataflowQueue
group = new DefaultPGroup(new ResizeablePool(true))
@TupleConstructor
class Greeter implements Callable<String> {
DataflowReadChannel names
DataflowWriteChannel greetings
@Override
String call() {
while(!Thread.currentThread().isInterrupted()) {
String name = names.val
greetings << "Hello " + name
}
return "CSP is great!"
}
}
def a = new SyncDataflowQueue()
def b = new SyncDataflowQueue()
group.task new Greeter(a, b)
a << "Joe"
a << "Dave"
println b.val
println b.val
組合
將程序分組只需將它們與通道連接即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
group = new DefaultPGroup(new ResizeablePool(true))
@TupleConstructor
class Formatter implements Callable<String> {
DataflowReadChannel rawNames
DataflowWriteChannel formattedNames
@Override
String call() {
while(!Thread.currentThread().isInterrupted()) {
String name = rawNames.val
formattedNames << name.toUpperCase()
}
}
}
@TupleConstructor
class Greeter implements Callable<String> {
DataflowReadChannel names
DataflowWriteChannel greetings
@Override
String call() {
while(!Thread.currentThread().isInterrupted()) {
String name = names.val
greetings << "Hello " + name
}
}
}
def a = new SyncDataflowQueue()
def b = new SyncDataflowQueue()
def c = new SyncDataflowQueue()
group.task new Formatter(a, b)
group.task new Greeter(b, c)
a << "Joe"
a << "Dave"
println c.val
println c.val
替代方案
為了引入非決定性,GPars 提供了 Select 類別及其 select 和 prioritySelect 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import groovy.transform.TupleConstructor
import groovyx.gpars.dataflow.SyncDataflowQueue
import groovyx.gpars.dataflow.DataflowReadChannel
import groovyx.gpars.dataflow.DataflowWriteChannel
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool
import static groovyx.gpars.dataflow.Dataflow.select
group = new DefaultPGroup(new ResizeablePool(true))
@TupleConstructor
class Receptionist implements Runnable {
DataflowReadChannel emails
DataflowReadChannel phoneCalls
DataflowReadChannel tweets
DataflowWriteChannel forwardedMessages
private final Select incomingRequests = select([phoneCalls, emails, tweets]) //prioritySelect() would give highest precedence to phone calls
@Override
void run() {
while(!Thread.currentThread().isInterrupted()) {
String msg = incomingRequests.select()
forwardedMessages << msg.toUpperCase()
}
}
}
def a = new SyncDataflowQueue()
def b = new SyncDataflowQueue()
def c = new SyncDataflowQueue()
def d = new SyncDataflowQueue()
group.task new Receptionist(a, b, c, d)
a << "my email"
b << "my phone call"
c << "my tweet"
//The values come in random order since the process uses a Select to read its input
3.times{
println d.val.value
}
元件
CSP 程序可以組合成更大的實體。假設您已經有一組 CSP 程序(又名 Runnable/Callable 類別),您可以將它們組合成一個更大的程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final class Prefix implements Callable {
private final DataflowChannel inChannel
private final DataflowChannel outChannel
private final def prefix
def Prefix(final inChannel, final outChannel, final prefix) {
this.inChannel = inChannel;
this.outChannel = outChannel;
this.prefix = prefix
}
public def call() {
outChannel << prefix
while (true) {
sleep 200
outChannel << inChannel.val
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final class Copy implements Callable {
private final DataflowChannel inChannel
private final DataflowChannel outChannel1
private final DataflowChannel outChannel2
def Copy(final inChannel, final outChannel1, final outChannel2) {
this.inChannel = inChannel;
this.outChannel1 = outChannel1;
this.outChannel2 = outChannel2;
}
public def call() {
final PGroup group = Dataflow.retrieveCurrentDFPGroup()
while (true) {
def i = inChannel.val
group.task {
outChannel1 << i
outChannel2 << i
}.join()
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import groovyx.gpars.dataflow.DataflowChannel
import groovyx.gpars.dataflow.SyncDataflowQueue
import groovyx.gpars.group.DefaultPGroup
group = new DefaultPGroup(6)
def fib(DataflowChannel out) {
group.task {
def a = new SyncDataflowQueue()
def b = new SyncDataflowQueue()
def c = new SyncDataflowQueue()
def d = new SyncDataflowQueue()
[new Prefix(d, a, 0L), new Prefix(c, d, 1L), new Copy(a, b, out), new StatePairs(b, c)].each { group.task it}
}
}
final SyncDataflowQueue ch = new SyncDataflowQueue()
group.task new Print('Fibonacci numbers', ch)
fib(ch)
sleep 10000

Actors (演員) 使用者指南
Actors 提供了一個基於訊息傳遞的並行模型:程式是由交換訊息且沒有可變共享狀態的獨立活動物件的集合組成。
Actors 可以幫助我們避免死鎖、活鎖和飢餓等問題,這些問題是基於共享記憶體方法常見的問題。
Actors 是一種利用當今硬體多核心特性的方法,而無需傳統上與共享記憶體多執行緒相關的所有問題,這也是為什麼 Erlang 和 Scala 等程式語言採用此模型的原因。
GPars 中的 Actor 支援最初受到 Scala 中 Actors 程式庫的啟發,但此後已遠遠超出 Scala 標準提供的功能。 |
Ruben Vermeersch 撰寫了一篇很好的文章,總結了 actors 背後的關鍵概念。
Actors 始終保證在任何時間點都最多只有一個執行緒處理 actor 的主體,並且在底層,每次將執行緒分配給 actor 時,都會同步記憶體,因此 actor 的狀態可以安全地被主體中的程式碼修改,而無需任何額外的(同步或鎖定)操作。
理想情況下,actor 的程式碼永遠不應直接從外部呼叫,因此 actor 類別的所有程式碼只能由處理最後收到的訊息的執行緒執行,因此 actor 的所有程式碼都隱含地具有執行緒安全性。
如果允許其他物件直接呼叫 actor 的任何方法,則 actor 程式碼和狀態的執行緒安全性保證將不再有效。
Actors (演員) 的類型
一般來說,您可以在野外找到兩種型別的 actor - 一種是具有隱含狀態,另一種則沒有。
GPars 為您提供這兩種選項。
無狀態的 actor,在 GPars 中由 DynamicDispatchActor 和 ReactiveActor 類別表示,它們不追蹤先前收到的訊息。您可以將它們視為平面訊息處理器,它們會依訊息的到達順序處理訊息。任何基於狀態的行為都必須由使用者實作。
有狀態的 actor,在 GPars 中由 DefaultActor 類別(以及之前的 AbstractPooledActor 類別)表示,允許我們直接處理隱含狀態。收到訊息後,actor 會進入新的狀態,以不同的方式處理未來的訊息。
舉例來說,剛啟動的 actor 可能只接受某些型別的訊息,例如,只有在收到加密金鑰後,才能接受加密訊息進行解密。有狀態的 actor 允許直接在訊息處理程式碼的結構中編碼這種相依性。然而,隱含狀態管理會產生輕微的效能成本,這主要是因為 JVM 上缺乏續傳支援。
Actor 線程模型
由於 actor 與系統執行緒分離,因此大量的 actor 可以共享相對較小的執行緒池。
這甚至可以達到許多並行 actor 共享單一池化執行緒,同時避免 JVM 的某些執行緒限制。
一般來說,雖然 JVM 只能給您有限數量的執行緒(通常約為數千個),但 actor 的數量僅受可用記憶體的限制。如果 actor 沒有任何工作要做,它就不會消耗任何執行緒。
Actor 程式碼以區塊方式處理,區塊之間會穿插等待新事件(訊息)的安靜期。這可以透過續傳自然地建模。
由於 JVM 不直接支援續傳,因此必須在 actor 框架中模擬它們,這會對 actor 程式碼的組織產生輕微的影響。但是,在大多數情況下,好處大於困難。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import groovyx.gpars.actor.Actor
import groovyx.gpars.actor.DefaultActor
class GameMaster extends DefaultActor {
int secretNum
void afterStart() {
secretNum = new Random().nextInt(10)
}
void act() {
loop {
react { int num ->
if (num > secretNum) {
reply 'too large'
}
else if (num < secretNum) {
reply 'too small'
}
else {
reply 'you win'
terminate()
}
}
}
}
}
class Player extends DefaultActor {
String name
Actor server
int myNum
void act() {
loop {
myNum = new Random().nextInt(10)
server.send myNum
react {
switch (it) {
case 'too large': println "$name: $myNum was too large"; break
case 'too small': println "$name: $myNum was too small"; break
case 'you win': println "$name: I won $myNum"; terminate(); break
}
}
}
}
}
def master = new GameMaster().start()
def player = new Player(name: 'Player', server: master).start()
// This forces the main thread to wait until both actors have terminated.
[master, player]*.join()
Jordi Campos i Miralles, Departament de Matemàtica Aplicada i Anàlisi, MAiA Facultat de Matemàtiques, Universitat de Barcelona 的範例
Actors (演員) 的用法
GPars 提供了與 Actor 相符的 API 和 DSL。原則上,actor 執行三個特定操作 - 發送訊息、接收訊息和建立新的 actor。雖然 GPars 並未特別強制執行,但訊息應為不可變的,或者至少在訊息發送後,發送者永遠不應觸碰該訊息,遵循放手原則。
發送訊息
可以使用 send 方法將訊息發送給 actor。
1
2
3
4
5
6
7
8
def passiveActor = Actors.actor{
loop {
react { msg -> println "Received: $msg"; }
}
}
passiveActor.send 'Message 1'
passiveActor << 'Message 2' //using the << operator
passiveActor 'Message 3' //using the implicit call() method
或者,可以使用 << 運算子或隱含的 call 方法。有一系列的 sendAndWait 方法可用於封鎖呼叫端,直到收到 actor 的回覆為止。reply 會從 sendAndWait 方法以回傳值的形式回傳。sendAndWait 方法也可能在逾時到期或呼叫的 actor 終止時回傳。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def replyingActor = Actors.actor{
loop {
react { msg ->
println "Received: $msg";
reply "I've got $msg"
}
}
}
def reply1 = replyingActor.sendAndWait('Message 4')
def reply2 = replyingActor.sendAndWait('Message 5', 10, TimeUnit.SECONDS)
use (TimeCategory) {
def reply3 = replyingActor.sendAndWait('Message 6', 10.seconds)
}
sendAndContinue 方法允許呼叫端繼續處理,同時提供的閉包正在等待 actor 的回覆。
1
2
friend.sendAndContinue 'I need money!', {money -> pocket money}
println 'I can continue while my friend is collecting money for me'
sendAndPromise 方法會回傳一個最終回覆的 Promise(又名 Future),因此允許呼叫端在 actor 處理提交的訊息時繼續處理。
1
2
3
4
Promise loan = friend.sendAndPromise 'I need money!'
println 'I can continue while my friend is collecting money for me'
loan.whenBound {money -> pocket money} // Asynchronous waiting for a reply.
println "Received ${loan.get()}" // Synchronous waiting for a reply.
如果在非作用中 actor 上呼叫所有 send 、sendAndWait 或 sendAndContinue 方法,則會擲回例外。
接收訊息
非封鎖訊息擷取
從 actor 的程式碼中呼叫 react 方法(可選擇使用逾時參數)將會從 actor 的收件匣中取用下一個訊息,如果沒有要立即處理的訊息,則可能會等待。
1
2
3
4
println 'Waiting for a gift'
react {gift ->
if (mySpouse.likes gift) reply 'Thank you!'
}
在底層,提供的閉包不會直接呼叫,而是安排由執行緒池中的任何執行緒在訊息可用時進行處理。在排程之後,目前的執行緒將與 actor 分離,並釋放以處理任何其他已經收到訊息的 actor。
為了允許將 actor 與執行緒分離,react 方法要求程式碼以特殊的續傳樣式撰寫。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Actors.actor {
loop {
println 'Waiting for a gift'
react {gift ->
if (mySpouse.likes gift) reply 'Thank you!'
else {
reply 'Try again, please'
react {anotherGift ->
if (myChildren.like gift) reply 'Thank you!'
}
println 'Never reached'
}
}
println 'Never reached'
}
println 'Never reached'
}
react 方法具有特殊的語意,允許在 actor 的信箱中沒有訊息可用時將 actor 與執行緒分離。基本上,react 會排程提供的程式碼(閉包)在下一個訊息到達時執行,並回傳。提供給 react 方法的閉包是計算應該繼續的程式碼。這是一種續傳樣式。
由於 actor 必須保留在 actor 的主體中最多有一個執行緒處於作用中的保證,因此在目前的訊息處理完成之前,無法處理下一個訊息。通常,在呼叫 react 之後不應該需要放置程式碼。有些 actor 實作甚至會強制執行這一點。然而,GPars 不會這樣做,為了效能考量。loop 方法允許在 actor 主體中進行迭代。與典型的迴圈結構(例如 for 或 while 迴圈)不同,loop 會與巢狀的 react 區塊協同運作,並確保在後續的訊息擷取中進行迴圈。
發送回覆
reply 和 replyIfExists 方法不僅在 actor 本身上定義,而且對於 AbstractPooledActor(DefaultActor、DynamicDispatchActor 和 ReactiveActor 類別中不可用),也在接收到的已處理訊息本身上定義,這在單次呼叫中處理多個訊息時特別方便。在這種情況下,在 actor 上呼叫的 reply() 將會向所有目前已處理訊息(最後一個訊息)的作者發送回覆,而對訊息呼叫的 reply() 則只會向該特定訊息的作者發送回覆。
請參閱我們範例演示中的 DemoMultiMessage.groovy |
Sender 屬性
訊息擷取時,會提供 sender 屬性來識別訊息的發起者。該屬性在 Actor 的閉包內可用。
1
2
3
4
react {tweet ->
if (isSpam(tweet)) ignoreTweetsFrom sender
sender.send 'Never write to me again!'
}
轉發
當發送訊息時,可以將不同的 actor 指定為發送者,以便將該訊息的潛在回覆轉發給指定的 actor,而不是實際的發起者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def decryptor = Actors.actor {
react {message ->
reply message.reverse()
// sender.send message.reverse() //An alternative way to send replies
}
}
def console = Actors.actor { //This actor will print out decrypted messages, since the replies are forwarded to it
react {
println 'Decrypted message: ' + it
}
}
decryptor.send 'lellarap si yvoorG', console //Specify an actor to send replies to
console.join()
建立 Actors
Actors 共享一個執行緒池,當 actor 需要對發送給它們的訊息做出反應時,這些執行緒會動態分配給 actor。一旦訊息被處理並且 actor 處於閒置狀態等待更多訊息到達時,執行緒就會返回到池中。
例如,以下是如何建立一個 actor 來印出它收到的所有訊息。
1
2
3
4
5
6
7
def console = Actors.actor {
loop {
react {
println it
}
}
}
請注意 loop() 方法呼叫,它可確保 actor 在處理完第一個訊息後不會停止。
以下是一個解密服務的範例,它可以解密提交的訊息,並將解密後的訊息發送回給發起者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final def decryptor = Actors.actor {
loop {
react {String message ->
if ('stopService' == message) {
println 'Stopping decryptor'
stop()
}
else reply message.reverse()
}
}
}
Actors.actor {
decryptor.send 'lellarap si yvoorG'
react {
println 'Decrypted message: ' + it
decryptor.send 'stopService'
}
}.join()
以下是一個 actor 的範例,它會等待最多 30 秒以接收其訊息的回覆。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def friend = Actors.actor {
react {
//this doesn't reply -> caller won't receive any answer in time
println it
//reply 'Hello' //uncomment this to answer conversation
react {
println it
}
}
}
def me = Actors.actor {
friend.send('Hi')
//wait for answer 1sec
react(1000) {msg ->
if (msg == Actor.TIMEOUT) {
friend.send('I see, busy as usual. Never mind.')
stop()
} else {
//continue conversation
println "Thank you for $msg"
}
}
}
me.join()
未送達的訊息
有時訊息無法傳遞到目標 actor。當需要針對未送達的訊息採取特殊行動時,在 actor 終止時,其佇列中的所有未處理訊息都會呼叫其 onDeliveryError() 方法。在訊息上定義的 onDeliveryError() 方法或閉包可以例如將通知發送回訊息的原始發送者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
final DefaultActor me
me = Actors.actor {
def message = 1
message.metaClass.onDeliveryError = {->
//send message back to the caller
me << "Could not deliver $delegate"
}
def actor = Actors.actor {
react {
//wait 2sec in order next call in demo can be emitted
Thread.sleep(2000)
//stop actor after first message
stop()
}
}
actor << message
actor << message
react {
//print whatever comes back
println it
}
}
me.join()
或者,可以在發送者本身上指定 onDeliveryError() 方法。該方法可以動態新增
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
final DefaultActor me
me = Actors.actor {
def message1 = 1
def message2 = 2
def actor = Actors.actor {
react {
//wait 2sec in order next call in demo can be emitted
Thread.sleep(2000)
//stop actor after first message
stop()
}
}
me.metaClass.onDeliveryError = {msg ->
//callback on actor inaccessibility
println "Could not deliver message $msg"
}
actor << message1
actor << message2
actor.join()
}
me.join()
也可以在 actor 定義中靜態新增
1
2
3
4
5
6
class MyActor extends DefaultActor {
public void onDeliveryError(msg) {
println "Could not deliver message $msg"
}
...
}
加入 Actors
Actors 提供 join() 方法,允許呼叫端等待 actor 終止。也提供接受逾時的變體。當一次加入多個 actor 時,Groovy 的 spread-dot(*.)運算子會很方便。
1
2
3
4
def master = new GameMaster().start()
def player = new Player(name: 'Player', server: master).start()
[master, player]*.join()
條件和計數迴圈
loop() 方法允許指定條件或迭代次數,也可選擇伴隨一個閉包,在迴圈結束後呼叫 - 迴圈終止程式碼處理常式。
以下 actor 將迴圈三次以接收 3 個訊息,然後印出接收到的訊息的最大值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final Actor actor = Actors.actor {
def candidates = []
def printResult = {-> println "The best offer is ${candidates.max()}"}
loop(3, printResult) {
react {
candidates << it
}
}
}
actor 10
actor 30
actor 20
actor.join()
以下 actor 將接收訊息,直到收到大於 30 的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final Actor actor = Actors.actor {
def candidates = []
final Closure printResult = {-> println "Reached best offer - ${candidates.max()}"}
loop({-> candidates.max() < 30}, printResult) {
react {
candidates << it
}
}
}
actor 10
actor 20
actor 25
actor 31
actor 20
actor.join()
迴圈終止程式碼處理常式 可以使用 actor 的 react{},但不能使用 loop()。 |
自訂排程器
Actor 預設會利用標準的 JDK 並行程式庫。若要提供自訂的執行緒排程器,請在建立平行群組(PGroup 類別)時使用適當的建構子參數。所提供的排程器將會協調群組執行緒池中的執行緒。
也請參考眾多的 Actor 範例示範程式。
Actors (演員) 原則
Actor 會共用一個執行緒池,當 actor 需要對傳送給它們的訊息做出反應時,這些執行緒會動態地指派給 actor。一旦訊息處理完畢且 actor 處於閒置狀態等待更多訊息到達時,執行緒會返回到池中。Actor 與底層執行緒分離,因此一個相對較小的執行緒池可以服務潛在無限數量的 actor。在 actor 數量上幾乎無限的擴展性是基於事件的 actor 的主要優勢,這些 actor 與底層的實體執行緒分離。
以下是一些如何使用 actor 的範例。這是如何建立一個會印出它接收到的所有訊息的 actor。
1
2
3
4
5
6
7
8
import static groovyx.gpars.actor.Actors.actor
def console = actor {
loop {
react {
println it
}
}
請注意 loop() 方法呼叫,它可確保 actor 在處理完第一個訊息後不會停止。
作為替代方案,您可以擴展 DefaultActor 類別並覆寫 act() 方法。一旦您實例化了 actor,您需要啟動它,以便它附加到執行緒池並開始接受訊息。actor() 工廠方法將會處理啟動 actor 的工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
class CustomActor extends DefaultActor {
@Override
protected void act() {
loop {
react {
println it
}
}
}
}
def console=new CustomActor()
console.start()
可以使用多種方法將訊息傳送給 actor
1
2
3
4
console.send('Message')
console 'Message'
console.sendAndWait 'Message' //Wait for a reply
console.sendAndContinue 'Message', {reply -> println "I received reply: $reply"} //Forward the reply to a function
建立非同步服務
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import static groovyx.gpars.actor.Actors.actor
final def decryptor = actor {
loop {
react {String message->
reply message.reverse()
}
}
}
def console = actor {
decryptor.send 'lellarap si yvoorG'
react {
println 'Decrypted message: ' + it
}
}
console.join()
如您所見,您可以使用 actor() 方法建立新的 actor,並將 actor 的主體作為閉包參數傳入。在 actor 的主體內,您可以使用 loop() 來迭代、使用 react() 來接收訊息,並使用 reply() 向傳送目前正在處理的訊息的 actor 發送訊息。目前訊息的傳送者也可以通過 actor 的 sender 屬性取得。當解密器 actor 在呼叫 react() 時,在訊息佇列中找不到訊息時,react() 方法會放棄執行緒並將其返回到執行緒池,以便其他 actor 可以使用它。
只有在新的訊息到達 actor 的訊息佇列後,react() 方法的閉包才會被排定由池進行處理。基於事件的 actor 在內部模擬連續性 - actor 的工作 - 被分成順序執行的區塊,一旦收件匣中有訊息可用時就會呼叫這些區塊。單個 actor 的每個區塊都可以由執行緒池中的不同執行緒執行。
Groovy 具有閉包的彈性語法,讓我們的程式庫能夠提供多種定義 actor 的方式。例如,這是一個 actor 的範例,它會等待最多 30 秒來接收其訊息的回覆。Actor 允許使用由 org.codehaus.groovy.runtime.TimeCategory 類別定義的時間 DSL,來為 react() 方法指定逾時,前提是用戶將呼叫包裝在 TimeCategory 使用區塊內。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def friend = Actors.actor {
react {
//this doesn't reply -> caller won't receive any answer in time
println it
//reply 'Hello' //uncomment this to answer conversation
react {
println it
}
}
}
def me = Actors.actor {
friend.send('Hi')
//wait for answer 1sec
react(1000) {msg ->
if (msg == Actor.TIMEOUT) {
friend.send('I see, busy as usual. Never mind.')
stop()
} else {
//continue conversation
println "Thank you for $msg"
}
}
}
me.join()
當等待訊息時逾時到期時,會收到 Actor.TIMEOUT
訊息。如果 actor 上存在 onTimeout() 處理常式,也會被呼叫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def friend = Actors.actor {
react {
//this doesn't reply -> caller won't receive any answer in time
println it
//reply 'Hello' //uncomment this to answer conversation
react {
println it
}
}
}
def me = Actors.actor {
friend.send('Hi')
delegate.metaClass.onTimeout = {->
friend.send('I see, busy as usual. Never mind.')
stop()
}
//wait for answer 1sec
react(1000) {msg ->
if (msg != Actor.TIMEOUT) {
//continue conversation
println "Thank you for $msg"
}
}
}
me.join()
請注意使用 Groovy 元程式設計來動態定義 actor 的生命週期通知方法(例如 onTimeout())的可能性。顯然,當您決定為 actor 定義一個新的類別時,可以使用通常的方式定義生命週期方法。
1
2
3
4
5
6
7
8
9
class MyActor extends DefaultActor {
public void onTimeout() {
...
}
protected void act() {
...
}
}
Actor 保證非執行緒安全程式碼的執行緒安全性
Actor 保證在任何時候最多只有一個執行緒在處理 actor 的主體。在底層,每次將執行緒指派給 actor 時,記憶體都會同步。因此,actor 的狀態可以安全地被主體中的程式碼修改,而無需任何額外的(同步或鎖定)操作。
1
2
3
4
5
6
7
8
9
10
11
class MyCounterActor extends DefaultActor {
private Integer counter = 0
protected void act() {
loop {
react {
counter++
}
}
}
}
理想情況下,actor 的程式碼永遠不應從外部直接呼叫,因此 actor 類別的所有程式碼只能由處理最後接收到的訊息的執行緒執行。因此,actor 的所有程式碼都隱含地是執行緒安全的。如果允許其他物件直接呼叫 actor 的任何方法,則 actor 程式碼和狀態的執行緒安全保證將不再有效。
簡單計算機
這是一個更實際的範例,說明一個事件驅動的 actor 接收兩個數值訊息,將它們相加,並將結果傳送到主控台 actor。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import groovyx.gpars.group.DefaultPGroup
//not necessary, just showing that a single-threaded pool can still handle multiple actors
def group = new DefaultPGroup(1);
final def console = group.actor {
loop {
react {
println 'Result: ' + it
}
}
}
final def calculator = group.actor {
react {a ->
react {b ->
console.send(a + b)
}
}
}
calculator.send 2
calculator.send 3
calculator.join()
group.shutdown()
請注意,事件驅動的 actor 需要特別注意 react() 方法。由於事件驅動的 actor 需要將程式碼分割成可循序指派給不同執行緒的獨立區塊,並且 JVM 本身不支援連續性,因此這些區塊是人為建立的。react() 方法會建立下一個訊息處理常式。一旦目前的訊息處理常式完成,就會排定下一個訊息處理常式(連續性)。
並行合併排序範例
為了比較,我還包括一個更複雜的範例,使用 actor 對整數列表執行並行合併排序。您可以看到,由於 Groovy 的彈性,我們非常接近 Scala 模型,儘管我仍然懷念 Scala 用於訊息處理的模式匹配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import groovyx.gpars.group.DefaultPGroup
import static groovyx.gpars.actor.Actors.actor
Closure createMessageHandler(def parentActor) {
return {
react {List<Integer> message ->
assert message != null
switch (message.size()) {
case 0..1:
parentActor.send(message)
break
case 2:
if (message[0] <= message[1]) parentActor.send(message)
else parentActor.send(message[-1..0])
break
default:
def splitList = split(message)
def child1 = actor(createMessageHandler(delegate))
def child2 = actor(createMessageHandler(delegate))
child1.send(splitList[0])
child2.send(splitList[1])
react {message1 ->
react {message2 ->
parentActor.send merge(message1, message2)
}
}
}
}
}
}
def console = new DefaultPGroup(1).actor {
react {
println "Sorted array:\t${it}"
System.exit 0
}
}
def sorter = actor(createMessageHandler(console))
sorter.send([1, 5, 2, 4, 3, 8, 6, 7, 3, 9, 5, 3])
console.join()
def split(List<Integer> list) {
int listSize = list.size()
int middleIndex = listSize / 2
def list1 = list[0..<middleIndex]
def list2 = list[middleIndex..listSize - 1]
return [list1, list2]
}
List<Integer> merge(List<Integer> a, List<Integer> b) {
int i = 0, j = 0
final int newSize = a.size() + b.size()
List<Integer> result = new ArrayList<Integer>(newSize)
while ((i < a.size()) && (j < b.size())) {
if (a[i] <= b[j]) result << a[i++]
else result << b[j++]
}
if (i < a.size()) result.addAll(a[i..-1])
else result.addAll(b[j..-1])
return result
}
由於 actor 會重複使用池中的執行緒,因此無論在過程中建立多少個 actor,該腳本都可以在幾乎任何大小的執行緒池中使用。
Actor 生命週期方法
每個 Actor 都可以定義生命週期觀察方法,這些方法會在發生特定生命週期事件時呼叫。
-
afterStart() - 在 actor 啟動後立即呼叫。
-
afterStop(List undeliveredMessages) - 在 actor 停止後立即呼叫,並傳入佇列中所有未處理的訊息。
-
onInterrupt(InterruptedException e) - 當 actor 的執行緒被中斷時呼叫。無論如何,執行緒中斷都會導致停止 actor。
-
onTimeout() - 當在為目前正在封鎖的 react 方法指定的逾時時間內沒有訊息傳送給 actor 時呼叫。
-
onException(Throwable e) - 當 actor 的事件處理常式中發生例外狀況時呼叫。從此方法返回後,Actor 將會停止。
您可以在 Actor 類別中靜態定義方法,或將它們動態加入到 actor 的元類別中
1
2
3
4
5
6
7
8
9
10
11
12
class MyActor extends DefaultActor {
public void afterStart() {
...
}
public void onTimeout() {
...
}
protected void act() {
...
}
}
1
2
3
4
5
6
7
def myActor = actor {
delegate.metaClass.onException = {
log.error('Exception occurred', it)
}
...
}
池管理
Actor 可以組織成群組,並且預設情況下,始終有一個應用程式範圍的池化 actor 群組可用。而且,就像 Actors 抽象工廠一樣,可以用於在預設群組中建立 actor。自訂群組可以用作抽象工廠,以建立屬於這些群組的新 actor 執行個體。
1
2
3
4
5
6
7
8
9
def myGroup = new DefaultPGroup()
def actor1 = myGroup.actor {
...
}
def actor2 = myGroup.actor {
...
}
actor 的 parallelGroup 屬性指向它所屬的群組。預設情況下,它指向預設的 actor 群組,即 Actors.defaultActorPGroup,並且只能在 actor 啟動之前進行更改。
1
2
3
4
5
6
7
8
class MyActor extends StaticDispatchActor<Integer> {
private static PGroup group = new DefaultPGroup(100)
MyActor(...) {
this.parallelGroup = group
...
}
}
屬於同一群組的 actor 會共用該群組的底層執行緒池。預設情況下,該池包含 n + 1 個執行緒,其中 n 代表 JVM 偵測到的 CPU 數量。池大小可以明確設定,方法是設定 gpars.poolsize 系統屬性,或者針對每個 actor 群組個別設定。這是透過指定適當的建構子參數來完成的。
1
def myGroup = new DefaultPGroup(10) //the pool will contain 10 threads
執行緒池可以透過適當的 DefaultPGroup 類別進行操作,該類別會委派給執行緒池的 Pool 介面。例如,resize() 方法允許您隨時變更池大小,而 resetDefaultSize() 則將其設定回預設值。當您需要安全地完成所有任務、銷毀池並停止所有執行緒以有組織的方式退出 JVM 時,可以呼叫 shutdown() 方法。
1
2
3
4
5
6
7
8
9
10
11
... (n+1 threads in the default pool after startup)
Actors.defaultActorPGroup.resize 1 //use one-thread pool
... (1 thread in the pool)
Actors.defaultActorPGroup.resetDefaultSize()
... (n+1 threads in the pool)
Actors.defaultActorPGroup.shutdown()
作為建立常駐執行緒池的 DefaultPGroup 的替代方案,當需要非常駐執行緒時,可以使用 NonDaemonPGroup 類別。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def daemonGroup = new DefaultPGroup()
def actor1 = daemonGroup.actor {
...
}
def nonDaemonGroup = new NonDaemonPGroup()
def actor2 = nonDaemonGroup.actor {
...
}
class MyActor {
def MyActor() {
this.parallelGroup = nonDaemonGroup
}
void act() {...}
}
屬於同一群組的 actor 會共用底層執行緒池。透過池化 actor 群組,您可以分割您的 actor,以利用多個不同大小的執行緒池,並將資源指派給系統的不同元件,並調整其效能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def coreActors = new NonDaemonPGroup(5) //5 non-daemon threads pool
def helperActors = new DefaultPGroup(1) //1 daemon thread pool
def priceCalculator = coreActors.actor {
...
}
def paymentProcessor = coreActors.actor {
...
}
def emailNotifier = helperActors.actor {
...
}
def cleanupActor = helperActors.actor {
...
}
//increase size of the core actor group
coreActors.resize 6
//shutdown the group's pool once you no longer need the group to release resources
helperActors.shutdown()
一旦您不再需要自訂的池化 actor 群組及其 actor,請務必將它們關閉,以保留系統資源。
預設 Actor 群組
未變更其 parallelGroup 屬性或透過 Actors 類別上的任何工廠方法建立的 actor 可以共用一個通用群組 Actors.defaultActorPGroup。此群組使用可調整大小的執行緒池,上限為 1000 個執行緒。這讓您可以舒適地讓池自動調整以滿足 actor 的需求。另一方面,隨著 actor 數量的增加,池可能會變得太大且效率不高。建議您將 actor 分組到具有固定大小執行緒池的您自己的 PGroups 中,除了不重要的應用程式之外。
常見陷阱:應用程式在 Actor 未收到訊息時終止
最有可能的是您正在使用常駐執行緒和池(這是預設設定),並且您的主要執行緒已完成。在任何、部分或所有 actor 上呼叫 actor.join() 會封鎖主要執行緒,直到 actor 終止,從而使所有 actor 保持執行。
或者,使用 NonDaemonPGroup 的執行個體並將部分 actor 指派給這些群組。
1
2
def nonDaemonGroup = new NonDaemonPGroup()
def myActor = nonDaemonGroup.actor {...}
或者。一個範例
1
2
3
4
5
6
7
8
9
10
11
def nonDaemonGroup = new NonDaemonPGroup()
class MyActor extends DefaultActor {
def MyActor() {
this.parallelGroup = nonDaemonGroup
}
void act() {...}
}
def myActor = new MyActor()
封鎖 Actor
在某些情況下,您可能偏好使用封鎖 actor,而不是事件驅動的連續性樣式 actor。封鎖 actor 會在其整個生命週期中保留一個池化執行緒,包括等待訊息的時間。它們避免了一些執行緒管理開銷,因為它們在啟動後永遠不會爭奪執行緒,並且還讓您可以編寫直接的程式碼,而無需使用連續性樣式。由於它們僅進行封鎖,因此訊息讀取是透過 receive 方法進行的。顯然,並行執行的封鎖 actor 數量受到共用池中可用執行緒數量的限制。另一方面,與連續性樣式 actor 相比,封鎖 actor 通常可提供更好的效能,尤其是在 actor 的訊息佇列很少為空的情況下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def decryptor = blockingActor {
while (true) {
receive {message ->
if (message instanceof String) reply message.reverse()
else stop()
}
}
}
def console = blockingActor {
decryptor.send 'lellarap si yvoorG'
println 'Decrypted message: ' + receive()
decryptor.send false
}
[decryptor, console]*.join()
封鎖 actor 增加了調整應用程式效能的選項數量。它們尤其可能是您 actor 網路中高流量位置的良好候選者。
無狀態的 Actors (演員)
動態分派 Actor
DynamicDispatchActor 類別是一個 actor,允許訊息處理程式碼的替代結構。
一般而言,DynamicDispatchActor 會重複掃描訊息,並將到達的訊息分派到 actor 上定義的 onMessage(message) 方法之一。在底層,DynamicDispatchActor 利用 Groovy 的動態方法分派機制。由於與 DefaultActor 後代不同,DynamicDispatchActor 不是 ReactiveActor(如下所述)不需要在後續訊息接收之間隱式記住 actor 的狀態,因此它們提供了更好的效能特性,通常可與其他 actor 架構(例如 Scala Actors)相媲美。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.actor.Actors
import groovyx.gpars.actor.DynamicDispatchActor
final class MyActor extends DynamicDispatchActor {
void onMessage(String message) {
println 'Received string'
}
void onMessage(Integer message) {
println 'Received integer'
reply 'Thanks!'
}
void onMessage(Object message) {
println 'Received object'
sender.send 'Thanks!'
}
void onMessage(List message) {
println 'Received list'
stop()
}
}
final def myActor = new MyActor().start()
Actors.actor {
myActor 1
myActor ''
myActor 1.0
myActor(new ArrayList())
myActor.join()
}.join()
在某些情況下,通常當不需要為 actor 保留隱式的對話歷程相依狀態時,動態分派程式碼結構可能比使用巢狀 loop 和 react 陳述式的傳統結構更直觀。
DynamicDispatchActor 類別還提供了一個方便的功能,可以在 actor 建立時或稍後的任何時間使用 when 處理常式動態新增訊息處理常式,這些處理常式可以選擇包裝在 become 方法中
1
2
3
4
5
6
7
8
9
10
11
12
final Actor myActor = new DynamicDispatchActor().become {
when {String msg -> println 'A String'; reply 'Thanks'}
when {Double msg -> println 'A Double'; reply 'Thanks'}
when {msg -> println 'A something ...'; reply 'What was that?';stop()}
}
myActor.start()
Actors.actor {
myActor 'Hello'
myActor 1.0d
myActor 10 as BigDecimal
myActor.join()
}.join()
顯然,可以組合這兩種方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class MyDDA extends DynamicDispatchActor {
void onMessage(String message) {
println 'Received string'
}
void onMessage(Integer message) {
println 'Received integer'
}
void onMessage(Object message) {
println 'Received object'
}
void onMessage(List message) {
println 'Received list'
stop()
}
}
final def myActor = new MyDDA().become {
when {BigDecimal num -> println 'Received BigDecimal'}
when {Float num -> println 'Got a float'}
}.start()
Actors.actor {
myActor 'Hello'
myActor 1.0f
myActor 10 as BigDecimal
myActor.send([])
myActor.join()
}.join()
透過 when 註冊的動態訊息處理常式將優先於靜態的 onMessage 處理常式。
1
def fairActor = Actors.fairMessageHandler {...}
靜態分派 Actor
DynamicDispatchActor 根據訊息的執行時期類型分派訊息,因此每個訊息都會產生額外的效能損失,而 StaticDispatchActor 則避免執行時期訊息檢查,僅根據編譯時資訊分派訊息。
1
2
3
4
5
6
7
8
9
10
11
12
13
final class MyActor extends StaticDispatchActor<String> {
void onMessage(String message) {
println 'Received string ' + message
switch (message) {
case 'hello':
reply 'Hi!'
break
case 'stop':
stop()
}
}
}
StaticDispatchActor 的實例必須覆寫適用於 actor 宣告類型參數的 onMessage 方法。然後,每次收到訊息時都會調用 onMessage(T message) 方法。
透過輔助工廠方法,可以更快速地建立公平和不公平的靜態分派 actor。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final actor = staticMessageHandler {String message ->
println 'Received string ' + message
switch (message) {
case 'hello':
reply 'Hi!'
break
case 'stop':
stop()
}
}
println 'Reply: ' + actor.sendAndWait('hello')
actor 'bye'
actor 'stop'
actor.join()
與 DynamicDispatchActor 相比,StaticDispatchActor 類別僅限於單一處理常式方法。
這種簡化的建立方式,無需任何 when 處理常式,加上顯著的效能優勢,應該使 StaticDispatchActor 成為您處理簡單訊息的首選。當不需要根據訊息執行時期類型分派時,請使用此方法。
例如,StaticDispatchActor 使資料流運算子的速度比 DynamicDispatchActor 快四倍。
反應式 Actor
ReactiveActor 類別,通常透過呼叫 Actors.reactor() 或 DefaultPGroup.reactor() 建立,允許更多事件驅動的方法。
當反應式 actor 收到訊息時,提供的程式碼區塊 (構成反應式 actor 的主體) 會以訊息作為參數執行。程式碼傳回的結果會作為回覆傳送。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final def group = new DefaultPGroup()
final def doubler = group.reactor {
2 * it
}
group.actor {
println 'Double of 10 = ' + doubler.sendAndWait(10)
}
group.actor {
println 'Double of 20 = ' + doubler.sendAndWait(20)
}
group.actor {
println 'Double of 30 = ' + doubler.sendAndWait(30)
}
for(i in (1..10)) {
println "Double of $i = ${doubler.sendAndWait(i)}"
}
doubler.stop()
doubler.join()
以下是一個 actor 的範例,它將一批數字提交給 ReactiveActor 進行處理,然後隨著結果到達逐步列印結果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import groovyx.gpars.actor.Actor
import groovyx.gpars.actor.Actors
final def doubler = Actors.reactor {
2 * it
}
Actor actor = Actors.actor {
(1..10).each {doubler << it}
int i = 0
loop {
i += 1
if (i > 10) stop()
else {
react {message ->
println "Double of $i = $message"
}
}
}
}
actor.join()
doubler.stop()
doubler.join()
基本上,反應式 actor 為在迴圈中等待訊息、處理它們並傳回結果的 actor 提供了一種方便的捷徑。這是在內部反應式 actor 的示意圖。
1
2
3
4
5
6
7
8
9
10
11
public class ReactiveActor extends DefaultActor {
Closure body
void act() {
loop {
react {message ->
reply body(message)
}
}
}
}
1
def fairActor = Actors.fairReactor {...}
提示與技巧
結構化 Actor 的程式碼
當擴展 DefaultActor 類別時,您可以在 act() 方法中呼叫任何 actor 的方法,並在其中使用 react() 或 loop() 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MyDemoActor extends DefaultActor {
protected void act() {
handleA()
}
private void handleA() {
react {a ->
handleB(a)
}
}
private void handleB(int a) {
react {b ->
println a + b
reply a + b
}
}
}
final def demoActor = new MyDemoActor()
demoActor.start()
Actors.actor {
demoActor 10
demoActor 20
react {
println "Result: $it"
}
}.join()
請記住,我們所有範例中的 handleA() 和 handleB() 方法只會排程提供的訊息處理常式,以回應下一個到達的訊息,作為目前計算的延續。
或者,當使用 actor() 工廠方法時,您可以透過元類別以閉包的形式新增事件處理程式碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Actor demoActor = Actors.actor {
delegate.metaClass {
handleA = {->
react {a ->
handleB(a)
}
}
handleB = {a ->
react {b ->
println a + b
reply a + b
}
}
}
handleA()
}
Actors.actor {
demoActor 10
demoActor 20
react {
println "Result: $it"
}
}.join()
將 actor 設定為委派的閉包也可以用於結構化事件處理程式碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Closure handleB = {a ->
react {b ->
println a + b
reply a + b
}
}
Closure handleA = {->
react {a ->
handleB(a)
}
}
Actor demoActor = Actors.actor {
handleA.delegate = delegate
handleB.delegate = delegate
handleA()
}
Actors.actor {
demoActor 10
demoActor 20
react {
println "Result: $it"
}
}.join()
事件驅動的迴圈
在編寫事件驅動的 actor 時,請記住對 react() 和 loop() 方法的呼叫具有稍微不同的語意。
當您嘗試在您的 actor 中實作任何類型的迴圈時,這會變得有點挑戰。另一方面,如果您利用 react() 僅排程延續並傳回的事實,您可以遞迴呼叫方法而無需擔心堆疊溢位。請查看以下範例,其中使用這三種描述的技術來結構化 actor 的程式碼。
DefaultActor 的子類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyLoopActor extends DefaultActor {
protected void act() {
outerLoop()
}
private void outerLoop() {
react {a ->
println 'Outer: ' + a
if (a != 0) innerLoop()
else println 'Done'
}
}
private void innerLoop() {
react {b ->
println 'Inner ' + b
if (b == 0) outerLoop()
else innerLoop()
}
}
}
final def actor = new MyLoopActor().start()
actor 10
actor 20
actor 0
actor 0
actor.join()
增強 Actor 的元類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Actor actor = Actors.actor {
delegate.metaClass {
outerLoop = {->
react {a ->
println 'Outer: ' + a
if (a!=0) innerLoop()
else println 'Done'
}
}
innerLoop = {->
react {b ->
println 'Inner ' + b
if (b==0) outerLoop()
else innerLoop()
}
}
}
outerLoop()
}
actor 10
actor 20
actor 0
actor 0
actor.join()
使用 Groovy 閉包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Closure innerLoop
Closure outerLoop = {->
react {a ->
println 'Outer: ' + a
if (a!=0) innerLoop()
else println 'Done'
}
}
innerLoop = {->
react {b ->
println 'Inner ' + b
if (b==0) outerLoop()
else innerLoop()
}
}
Actor actor = Actors.actor {
outerLoop.delegate = delegate
innerLoop.delegate = delegate
outerLoop()
}
actor 10
actor 20
actor 0
actor 0
actor.join()
此外,別忘了使用 actor 的 loop() 方法建立一個迴圈,直到 actor 終止才結束的構想。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyLoopingActor extends DefaultActor {
protected void act() {
loop {
outerLoop()
}
}
private void outerLoop() {
react {a ->
println 'Outer: ' + a
if (a!=0) innerLoop()
else println 'Done for now, but will loop again'
}
}
private void innerLoop() {
react {b ->
println 'Inner ' + b
if (b == 0) outerLoop()
else innerLoop()
}
}
}
final def actor = new MyLoopingActor().start()
actor 10
actor 20
actor 0
actor 0
actor 10
actor.stop()
actor.join()
Active Objects (活動物件)
活動物件在 actor 之上提供了一個 OO 外觀。這使您可以避免直接處理 actor 機制,不必匹配訊息、等待結果和傳送回覆。哎呀!
具有友善外觀的 Actor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import groovyx.gpars.activeobject.ActiveObject
import groovyx.gpars.activeobject.ActiveMethod
@ActiveObject
class Decryptor {
@ActiveMethod
def decrypt(String encryptedText) {
return encryptedText.reverse()
}
@ActiveMethod
def decrypt(Integer encryptedNumber) {
return -1*encryptedNumber + 142
}
}
final Decryptor decryptor = new Decryptor()
def part1 = decryptor.decrypt(' noitcA ni yvoorG')
def part2 = decryptor.decrypt(140)
def part3 = decryptor.decrypt('noitide dn')
print part1.get()
print part2.get()
println part3.get()
您可以使用 @ActiveObject 註釋標記活動物件。這將確保為您的類別的每個實例建立一個隱藏的 actor 實例。現在您可以使用 @ActiveMethod 註釋標記方法,表示您希望目標物件的內部 actor 以非同步方式調用該方法。@ActiveMethod 註釋的可選布林值 blocking 參數指定呼叫者是否應阻塞直到結果可用,或者呼叫者是否應僅收到未來結果的 promise (以 DataflowVariable 的形式),因此呼叫者不會被阻塞等待。
在底層,GPars 會將您的方法呼叫轉換為 傳送給內部 actor 的訊息。actor 最終會透過代表呼叫者調用所需的方法來處理該訊息,並且一旦完成,就會將回覆傳回給呼叫者。非阻塞方法會傳回結果的 promise,也稱為 DataflowVariables 。
但是阻塞意味著我們並不是真正地非同步,對嗎?
確實,如果您將您的活動方法標記為 blocking ,則呼叫者會被阻塞等待結果,就像執行普通的純方法調用一樣。我們所達成的就是在活動物件內部實現執行緒安全,防止並行存取。synchronized 關鍵字也可以為您提供相同的效果。因此,應該是 非阻塞 方法驅使您做出使用活動物件的決定。然後,阻塞方法將提供通常的同步語意,同時保證並行方法調用之間的一致性。因此,阻塞方法在與非阻塞方法組合使用時仍然非常有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import groovyx.gpars.activeobject.ActiveMethod
import groovyx.gpars.activeobject.ActiveObject
import groovyx.gpars.dataflow.DataflowVariable
@ActiveObject
class Decryptor {
@ActiveMethod(blocking=true)
String decrypt(String encryptedText) {
encryptedText.reverse()
}
@ActiveMethod(blocking=true)
Integer decrypt(Integer encryptedNumber) {
-1*encryptedNumber + 142
}
}
final Decryptor decryptor = new Decryptor()
print decryptor.decrypt(' noitcA ni yvoorG')
print decryptor.decrypt(140)
println decryptor.decrypt('noitide dn')
非阻塞語意
呼叫非阻塞活動方法將在訊息傳送給 actor 後立即傳回。現在允許呼叫者執行任何操作,而 actor 則負責計算。
可以使用 promise 上的 bound 屬性輪詢計算狀態。呼叫傳回 promise 上的 get() 方法將阻塞呼叫者,直到值可用。對 get() 的呼叫最終將傳回一個值或拋出一個例外,具體取決於實際計算的結果。
get() 方法有一個帶逾時參數的變體,以避免無限期等待的風險。 |
註釋規則
註釋您的物件時,需要遵循一些規則
-
只有在註釋為 ActiveObject 的類別中,才接受 ActiveMethod 註釋
-
只有實例 (非靜態) 方法可以註釋為 ActiveMethod
-
您可以使用非活動方法覆寫活動方法,反之亦然
-
活動物件的子類別可以宣告其他活動方法,前提是它們本身也註釋為 ActiveObject
-
並行使用活動方法和非活動方法可能會導致競爭條件。理想情況下,將您的活動物件設計為完全封裝的類別,其中所有非私有方法都標記為活動方法
繼承
@ActiveObject 註釋可以出現在繼承階層中的任何類別上。actor 欄位僅會在階層中最頂層的註釋類別中建立,子類別將重複使用該欄位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import groovyx.gpars.activeobject.ActiveObject
import groovyx.gpars.activeobject.ActiveMethod
import groovyx.gpars.dataflow.DataflowVariable
@ActiveObject
class A {
@ActiveMethod
def fooA(value) {
...
}
}
class B extends A {
}
@ActiveObject
class C extends B {
@ActiveMethod
def fooC(value1, value2) {
...
}
}
在我們的範例中,actor 欄位將在類別 A 中產生。類別 C 必須使用 @ActiveObject 註釋,因為它在方法 fooC() 上保留了 @ActiveMethod 註釋,而類別 B 則不需要註釋,因為其方法都不是活動的。
群組
就像 actor 可以圍繞執行緒池分組一樣,活動物件也可以設定為使用特定平行群組中的執行緒。
1
2
3
4
@ActiveObject("group1")
class MyActiveObject {
...
}
@ActiveObject 註釋的 value 參數指定要將內部 actor 繫結到的平行群組名稱。只有指定群組中的執行緒才會用於執行類別實例的內部 actor。
但是,必須在建立屬於該群組的任何活動物件實例之前,建立和註冊這些群組。如果未明確指定,活動物件將使用預設的 actor 群組 - Actors.defaultActorPGroup 。
1
2
final DefaultPGroup group = new DefaultPGroup(10)
ActiveObjectRegistry.instance.register("group1", group)
內部 Actor 的替代名稱
您可能很少會遇到活動物件的內部 actor 欄位的預設名稱衝突。如果您需要變更預設名稱 internalActiveObjectActor ,請使用 @ActiveObject 註釋的 actorName 參數。
1
2
3
4
@ActiveObject(actorName = "alternativeActorName")
class MyActiveObject {
...
}
經典範例
Actor 使用的幾個範例
-
埃拉托斯特尼篩法
-
睡眠理髮師
-
哲學家進餐
-
單字排序
-
負載平衡器
埃拉托斯特尼篩法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import groovyx.gpars.actor.DynamicDispatchActor
/**
* Demonstrates concurrent implementation of the Sieve of Eratosthenes using actors
*
* In principle, the algorithm consists of concurrently run chained filters,
* each of which detects whether the current number can be divided by a single prime number.
* (generate nums 1, 2, 3, 4, 5, ...) -> (filter by mod 2) -> (filter by mod 3) -> (filter by mod 5) -> (filter by mod 7) -> (filter by mod 11) -> (caution! Primes falling out here)
* The chain is built (grows) on the fly, whenever a new prime is found.
*/
int requestedPrimeNumberBoundary = 1000
final def firstFilter = new FilterActor(2).start()
/**
* Generating candidate numbers and sending them to the actor chain
*/
(2..requestedPrimeNumberBoundary).each {
firstFilter it
}
firstFilter.sendAndWait 'Poison'
/**
* Filter out numbers that can be divided by a single prime number
*/
final class FilterActor extends DynamicDispatchActor {
private final int myPrime
private def follower
def FilterActor(final myPrime) { this.myPrime = myPrime; }
/**
* Try to divide the received number with the prime. If the number cannot be divided, send it along the chain.
* If there's no-one to send it to, I'm the last in the chain, the number is a prime and so I will create and chain
* a new actor responsible for filtering by this newly found prime number.
*/
def onMessage(int value) {
if (value % myPrime != 0) {
if (follower) follower value
else {
println "Found $value"
follower = new FilterActor(value).start()
}
}
}
/**
* Stop the actor on poisson reception
*/
def onMessage(def poisson) {
if (follower) {
def sender = sender
follower.sendAndContinue(poisson, {this.stop(); sender?.send('Done')}) //Pass the poisson along and stop after a reply
} else { //I am the last in the chain
stop()
reply 'Done'
}
}
}
睡眠理髮師
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.actor.DefaultActor
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.actor.Actor
final def group = new DefaultPGroup()
final def barber = group.actor {
final def random = new Random()
loop {
react {message ->
switch (message) {
case Enter:
message.customer.send new Start()
println "Barber: Processing customer ${message.customer.name}"
doTheWork(random)
message.customer.send new Done()
reply new Next()
break
case Wait:
println "Barber: No customers. Going to have a sleep"
break
}
}
}
}
private def doTheWork(Random random) {
Thread.sleep(random.nextInt(10) * 1000)
}
final Actor waitingRoom
waitingRoom = group.actor {
final int capacity = 5
final List<Customer> waitingCustomers = []
boolean barberAsleep = true
loop {
react {message ->
switch (message) {
case Enter:
if (waitingCustomers.size() == capacity) {
reply new Full()
} else {
waitingCustomers << message.customer
if (barberAsleep) {
assert waitingCustomers.size() == 1
barberAsleep = false
waitingRoom.send new Next()
}
else reply new Wait()
}
break
case Next:
if (waitingCustomers.size()>0) {
def customer = waitingCustomers.remove(0)
barber.send new Enter(customer:customer)
} else {
barber.send new Wait()
barberAsleep = true
}
}
}
}
}
class Customer extends DefaultActor {
String name
Actor localBarbers
void act() {
localBarbers << new Enter(customer:this)
loop {
react {message ->
switch (message) {
case Full:
println "Customer: $name: The waiting room is full. I am leaving."
stop()
break
case Wait:
println "Customer: $name: I will wait."
break
case Start:
println "Customer: $name: I am now being served."
break
case Done:
println "Customer: $name: I have been served."
stop();
break
}
}
}
}
}
class Enter { Customer customer }
class Full {}
class Wait {}
class Next {}
class Start {}
class Done {}
def customers = []
customers << new Customer(name:'Joe', localBarbers:waitingRoom).start()
customers << new Customer(name:'Dave', localBarbers:waitingRoom).start()
customers << new Customer(name:'Alice', localBarbers:waitingRoom).start()
sleep 15000
customers << new Customer(name: 'James', localBarbers: waitingRoom).start()
sleep 5000
customers*.join()
barber.stop()
waitingRoom.stop()
哲學家進餐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import groovyx.gpars.actor.DefaultActor
import groovyx.gpars.actor.Actors
Actors.defaultActorPGroup.resize 5
final class Philosopher extends DefaultActor {
private Random random = new Random()
String name
def forks = []
void act() {
assert 2 == forks.size()
loop {
think()
forks*.send new Take()
def messages = []
react {a ->
messages << [a, sender]
react {b ->
messages << [b, sender]
if ([a, b].any {Rejected.isCase it}) {
println "$name: \tOops, can't get my forks! Giving up."
final def accepted = messages.find {Accepted.isCase it[0]}
if (accepted!=null) accepted[1].send new Finished()
} else {
eat()
reply new Finished()
}
}
}
}
}
void think() {
println "$name: \tI'm thinking"
Thread.sleep random.nextInt(5000)
println "$name: \tI'm done thinking"
}
void eat() {
println "$name: \tI'm EATING"
Thread.sleep random.nextInt(2000)
println "$name: \tI'm done EATING"
}
}
final class Fork extends DefaultActor {
String name
boolean available = true
void act() {
loop {
react {message ->
switch (message) {
case Take:
if (available) {
available = false
reply new Accepted()
} else reply new Rejected()
break
case Finished:
assert !available
available = true
break
default: throw new IllegalStateException("Cannot process the message: $message")
}
}
}
}
}
final class Take {}
final class Accepted {}
final class Rejected {}
final class Finished {}
def forks = [
new Fork(name:'Fork 1'),
new Fork(name:'Fork 2'),
new Fork(name:'Fork 3'),
new Fork(name:'Fork 4'),
new Fork(name:'Fork 5')
]
def philosophers = [
new Philosopher(name:'Joe', forks:[forks[0], forks[1]]),
new Philosopher(name:'Dave', forks:[forks[1], forks[2]]),
new Philosopher(name:'Alice', forks:[forks[2], forks[3]]),
new Philosopher(name:'James', forks:[forks[3], forks[4]]),
new Philosopher(name:'Phil', forks:[forks[4], forks[0]]),
]
forks*.start()
philosophers*.start()
sleep 10000
forks*.stop()
philosophers*.stop()
單字排序
給定一個資料夾名稱,腳本會對資料夾中所有檔案中的單字進行排序。SortMaster actor 會建立給定數量的 WordSortActors ,在它們之間分割要排序單字的檔案,並收集結果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//Messages
private final class FileToSort { String fileName }
private final class SortResult { String fileName; List<String> words }
//Worker actor
class WordSortActor extends DefaultActor {
private List<String> sortedWords(String fileName) {
parseFile(fileName).sort {it.toLowerCase()}
}
private List<String> parseFile(String fileName) {
List<String> words = []
new File(fileName).splitEachLine(' ') {words.addAll(it)}
return words
}
void act() {
loop {
react {message ->
switch (message) {
case FileToSort:
println "Sorting file=${message.fileName} on thread ${Thread.currentThread().name}"
reply new SortResult(fileName: message.fileName, words: sortedWords(message.fileName))
}
}
}
}
}
//Master actor
final class SortMaster extends DefaultActor {
String docRoot = '/'
int numActors = 1
List<List<String>> sorted = []
private CountDownLatch startupLatch = new CountDownLatch(1)
private CountDownLatch doneLatch
private void beginSorting() {
int cnt = sendTasksToWorkers()
doneLatch = new CountDownLatch(cnt)
}
private List createWorkers() {
return (1..numActors).collect {new WordSortActor().start()}
}
private int sendTasksToWorkers() {
List<Actor> workers = createWorkers()
int cnt = 0
new File(docRoot).eachFile {
workers[cnt % numActors] << new FileToSort(fileName: it)
cnt += 1
}
return cnt
}
public void waitUntilDone() {
startupLatch.await()
doneLatch.await()
}
void act() {
beginSorting()
startupLatch.countDown()
loop {
react {
switch (it) {
case SortResult:
sorted << it.words
doneLatch.countDown()
println "Received results for file=${it.fileName}"
}
}
}
}
}
//start the actors to sort words
def master = new SortMaster(docRoot: 'c:/tmp/Logs/', numActors: 5).start()
master.waitUntilDone()
println 'Done'
File file = new File("c:/tmp/Logs/sorted_words.txt")
file.withPrintWriter { printer ->
master.sorted.each { printer.println it }
}
負載平衡器
展示了在可調整的工人集合之間進行工作負載平衡。負載平衡器接收任務並將它們排隊到臨時任務佇列中。當工人完成其分配時,它會向負載平衡器請求新任務。
如果負載平衡器的任務佇列中沒有任何可用任務,則會停止工人。如果任務佇列中的任務數量超過特定限制,則會建立新的工人以增加工人池的大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import groovyx.gpars.actor.Actor
import groovyx.gpars.actor.DefaultActor
/**
* Demonstrates work balancing among adaptable set of workers.
* The load balancer receives tasks and queues them in a temporary task queue.
* When a worker finishes his assignment, it asks the load balancer for a new task.
* If the load balancer doesn't have any tasks available in the task queue, the worker is stopped.
* If the number of tasks in the task queue exceeds certain limit, a new worker is created
* to increase size of the worker pool.
*/
final class LoadBalancer extends DefaultActor {
int workers = 0
List taskQueue = []
private static final QUEUE_SIZE_TRIGGER = 10
void act() {
loop {
react { message ->
switch (message) {
case NeedMoreWork:
if (taskQueue.size() == 0) {
println 'No more tasks in the task queue. Terminating the worker.'
reply DemoWorker.EXIT
workers -= 1
} else reply taskQueue.remove(0)
break
case WorkToDo:
taskQueue << message
if ((workers == 0) || (taskQueue.size() >= QUEUE_SIZE_TRIGGER)) {
println 'Need more workers. Starting one.'
workers += 1
new DemoWorker(this).start()
}
}
println "Active workers=${workers}\tTasks in queue=${taskQueue.size()}"
}
}
}
}
final class DemoWorker extends DefaultActor {
final static Object EXIT = new Object()
private static final Random random = new Random()
Actor balancer
def DemoWorker(balancer) {
this.balancer = balancer
}
void act() {
loop {
this.balancer << new NeedMoreWork()
react {
switch (it) {
case WorkToDo:
processMessage(it)
break
case EXIT: terminate()
}
}
}
}
private void processMessage(message) {
synchronized (random) {
Thread.sleep random.nextInt(5000)
}
}
}
final class WorkToDo {}
final class NeedMoreWork {}
final Actor balancer = new LoadBalancer().start()
//produce tasks
for (i in 1..20) {
Thread.sleep 100
balancer << new WorkToDo()
}
//produce tasks in a parallel thread
Thread.start {
for (i in 1..10) {
Thread.sleep 1000
balancer << new WorkToDo()
}
}
Thread.sleep 35000 //let the queues get empty
balancer << new WorkToDo()
balancer << new WorkToDo()
Thread.sleep 10000
balancer.stop()
balancer.join()

Agents (代理人) 使用者指南
Agent 類別是一個執行緒安全、非阻塞的共享可變狀態包裝器實作,靈感來自 Clojure 中的 Agents。
簡介
在 Clojure 程式語言中,您可以找到 Agents 的概念,其目的是保護需要在執行緒之間共享的可變資料。 Agents 隱藏了資料並保護它不受直接存取。客戶端只能將命令(函式)傳送到 agent。這些命令將會被序列化,並輪流逐一對資料進行處理。
由於命令是循序執行的,因此這些命令不需要擔心並行問題,並且可以在執行時假設資料完全屬於它們。雖然實作方式不同,但 GPars Agents,稱為 Agent,基本上行為類似於 actors。它們接受訊息並非同步地處理它們。然而,這些訊息必須是命令(函式或 Groovy 閉包),並且將在 agent 內部執行。接收後,收到的函式將針對 Agent 的內部狀態執行,並且該函式的傳回值將被視為 Agent 的新內部狀態。
本質上,agents 通過只允許單一的agent 管理的執行緒對它們進行修改來保護可變值。可變值無法從外部直接存取,而是必須將請求傳送到 agent,並且保證 agent 代表呼叫者循序處理這些請求。 Agents 保證所有請求的循序執行,因此可以確保數值的一致性。
1
2
3
4
5
6
7
8
agent = new Agent(0) //created a new Agent wrapping an integer with initial value 0
agent.send {increment()} //asynchronous send operation, sending the increment() function
...
//after some delay to process the message the internal Agent's state has been updated
...
assert agent.val== 1
要包裝整數,我們當然可以在 Java 平台上使用 AtomicXXX
類型,但是當狀態是一個更複雜的物件時,我們需要更多支援。
概念
GPars 提供了一個 Agent 類別,這是一個特殊的、執行緒安全、非阻塞的實作,靈感來自 Clojure 中的 Agents。
Agent 包裝了對可變狀態的參考,該參考保存在單一欄位中,並接受程式碼(閉包或命令)作為訊息,可以使用 '<<' 運算子、send() 方法或隱式呼叫 () 方法將這些訊息傳送到 Agent,就像傳送到任何其他 actor 一樣。
在接收到閉包/命令後的某個時間點,閉包會針對內部可變欄位進行調用,並且可以對其進行變更。保證閉包的執行不會受到其他執行緒的干擾,因此可以自由地變更保存在內部 data 欄位中的 Agent 內部狀態。
整個更新過程屬於 fire-and-forget
類型,因為一旦將訊息(閉包)傳送到 Agent,呼叫執行緒就可以去做其他事情,稍後再使用 Agent.val 或 Agent.valAsync(closure) 來檢查目前的值。
基本規則
-
執行時,提交的命令會將 agent 的狀態作為參數取得。
-
提交的命令/閉包可以呼叫 *agent8 的狀態上的任何方法。
-
也可以用新的物件替換狀態物件,並使用 updateValue() 方法完成。
-
提交的閉包的傳回值沒有特殊含義,並且會被忽略。
-
如果傳送到 Agent 的訊息
不是閉包
,則會將其視為內部參考欄位的新值。 -
Agent 的 val 屬性會等到 agent 佇列中的所有先前命令都被使用完畢,然後安全地傳回 Agent 的值。
-
valAsync() 方法也會做相同的事情,不會阻塞呼叫者。
-
instantVal 屬性會傳回內部 agent 狀態的立即快照。
-
所有 Agent 實例都共用一個預設的守護執行緒集區。設定 Agent 實例的 threadPool 屬性將允許它使用不同的執行緒集區。
-
可以使用 errors 屬性來收集命令擲出的例外狀況。
範例
成員的共享清單
Agent 包裝了已加入俱樂部的成員清單。要新增新成員,必須將訊息(新增成員的命令)傳送到 clubMembers Agent。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import groovyx.gpars.agent.Agent import
java.util.concurrent.ExecutorService import java.util.concurrent.Executors
/**
* Create a new Agent wrapping a list of strings
*/
def clubMembers = new Agent<List<String>>(['Me']) //add Me
clubMembers.send {it.add 'James'} //add James
final Thread t1 = Thread.start {
clubMembers.send {it.add 'Joe'} //add Joe
}
final Thread t2 = Thread.start {
clubMembers << {it.add 'Dave'} //add Dave
clubMembers {it.add 'Alice'} //add Alice (using the implicit call() method)
}
[t1, t2]*.join()
println clubMembers.val
clubMembers.valAsync {println "Current members: $it"}
clubMembers.await()
共享會議的註冊人數計數
Conference 類別允許註冊和取消註冊,但是這些方法只能從傳送到 conference Agent 的命令中呼叫。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.agent.Agent
/**
* Conference stores number of registrations and allows parties to register and unregister.
* It inherits from the Agent class and adds the register() and unregister() private methods,
* which callers may use it the commands they submit to the Conference.
*/
class Conference extends Agent<Long> {
def Conference() { super(0) }
private def register(long num) { data += num }
private def unregister(long num) { data -= num }
}
final Agent conference = new Conference() //new Conference created
/**
* Three external parties will try to register/unregister concurrently
*/
final Thread t1 = Thread.start {
conference << {register(10L)} //send a command to register 10 attendees
}
final Thread t2 = Thread.start {
conference << {register(5L)} //send a command to register 5 attendees
}
final Thread t3 = Thread.start {
conference << {unregister(3L)} //send a command to unregister 3 attendees
}
[t1, t2, t3]*.join()
assert 12L == conference.val
工廠方法
Agent 實例也可以使用 Agent.agent() 工廠方法建立。
1
def clubMembers = Agent.agent ['Me'] //add Me
監聽器與驗證器
Agents 允許使用者新增監聽器和驗證器。當內部狀態每次變更時,會通知監聽器,而驗證器可以透過擲出例外狀況來拒絕或否決即將發生的變更。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final Agent counter = new Agent()
counter.addListener {oldValue, newValue -> println "Changing value from $oldValue to $newValue"}
counter.addListener {agent, oldValue, newValue -> println "Agent $agent changing value from $oldValue to $newValue"}
counter.addValidator {oldValue, newValue -> if (oldValue > newValue) throw new IllegalArgumentException('Things can only go up in Groovy')}
counter.addValidator {agent, oldValue, newValue -> if (oldValue == newValue) throw new IllegalArgumentException('Things never stay the same for $agent')}
counter 10
counter 11
counter {updateValue 12}
counter 10 //Will be rejected
counter {updateValue it - 1} //Will be rejected
counter {updateValue it} //Will be rejected
counter {updateValue 11} //Will be rejected
counter 12 //Will be rejected
counter 20
counter.await()
監聽器和驗證器本質上都是採用兩個或三個參數的閉包。從驗證器擲出的例外狀況將會記錄在 agent 內部,並且可以使用 hasErrors() 方法進行測試,或透過 errors 屬性擷取。
1
2
assert counter.hasErrors()
assert counter.errors.size() == 5
驗證器的陷阱
Groovy 對於變數資料類型和不可變性的要求不是很嚴格,因此 agent 使用者應該了解潛在的障礙。
如果提交的程式碼直接修改狀態,則驗證器將無法在違反驗證規則時撤銷變更。有兩種可行的解決方案
-
確保您永遠不要變更表示目前 agent 狀態的供應物件
-
在 agent 上使用自訂複製策略,以允許 agent 建立內部狀態的副本
在這兩種情況下,您都需要呼叫 updateValue() 來設定並正確驗證新狀態。
問題以及兩種解決方案如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Create an agent storing names, rejecting 'Joe'
final Closure rejectJoeValidator = {oldValue, newValue -> if ('Joe' in newValue) throw new IllegalArgumentException('Joe is not allowed to enter our list.')}
Agent agent = new Agent([])
agent.addValidator rejectJoeValidator
agent {it << 'Dave'} //Accepted
agent {it << 'Joe'} //Erroneously accepted, since by-passes the validation mechanism
println agent.val
//Solution 1 - never alter the supplied state object
agent = new Agent([])
agent.addValidator rejectJoeValidator
agent {updateValue(['Dave', * it])} //Accepted
agent {updateValue(['Joe', * it])} //Rejected
println agent.val
//Solution 2 - use custom copy strategy on the agent
agent = new Agent([], {it.clone()})
agent.addValidator rejectJoeValidator
agent {updateValue it << 'Dave'} //Accepted
agent {updateValue it << 'Joe'} //Rejected, since 'it' is now just a copy of the internal agent's state
println agent.val
分組
依預設,所有 Agent 實例都屬於同一個群組,共用其守護執行緒集區。
自訂群組也可以建立 Agent 的實例。這些實例將屬於建立它們的群組,並且會共用一個執行緒集區。要建立屬於群組的 Agent 實例,請在群組上呼叫 agent() 工廠方法。這樣您就可以組織和調整 agents 的效能。
1
2
final def group = new NonDaemonPGroup(5) //create a group around a thread pool
def clubMembers = group.agent(['Me']) //add Me
直接集區取代
或者,透過在 Agent 實例上呼叫 attachToThreadPool() 方法,可以為它指定自訂執行緒集區。
1
2
3
4
def clubMembers = new Agent<List<String>>(['Me']) //add Me
final ExecutorService pool = Executors.newFixedThreadPool(10)
clubMembers.attachToThreadPool(new DefaultPool(pool))
請記住,就像 actors 一樣,單個 Agent 實例(又稱 agent)永遠不能一次使用一個以上的執行緒 |
購物車範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import groovyx.gpars.agent.Agent
class ShoppingCart {
private def cartState = new Agent([:])
//----------------- public methods below here ----------------------------------
public void addItem(String product, int quantity) {
cartState << {it[product] = quantity} //the << operator sends
//a message to the Agent
} public void removeItem(String product) {
cartState << {it.remove(product)}
} public Object listContent() {
return cartState.val
} public void clearItems() {
cartState << performClear
}
public void increaseQuantity(String product, int quantityChange) {
cartState << this.&changeQuantity.curry(product, quantityChange)
}
//----------------- private methods below here ---------------------------------
private void changeQuantity(String product, int quantityChange, Map items) {
items[product] = (items[product] ?: 0) + quantityChange
} private Closure performClear = { it.clear() }
}
//----------------- script code below here -------------------------------------
final ShoppingCart cart = new ShoppingCart()
cart.addItem 'Pilsner', 10
cart.addItem 'Budweisser', 5
cart.addItem 'Staropramen', 20
cart.removeItem 'Budweisser'
cart.addItem 'Budweisser', 15
println "Contents ${cart.listContent()}"
cart.increaseQuantity 'Budweisser', 3
println "Contents ${cart.listContent()}"
cart.clearItems()
println "Contents ${cart.listContent()}"
您可能已經注意到程式碼中的兩種實作策略。
-
公用方法可以在內部僅將所需的程式碼傳送到 Agent,而不是直接執行相同的功能
1
2
3
public void addItem(String product, int quantity) {
cartState[product]=quantity
}
1
2
3
public void addItem(String product, int quantity) {
cartState << {it[product] = quantity}
}
-
公用方法可以傳送對內部私有方法或閉包的參考,這些方法或閉包持有要執行動作的所需功能。
1
2
3
4
5
public void clearItems() {
cartState << performClear
}
private Closure performClear = { it.clear() }
如果閉包除了目前的內部狀態實例之外還採用其他引數,則可能需要柯里化。請參閱 increaseQuantity 方法。
印表機服務範例
另一個範例 - 假設多個執行緒共用一個非執行緒安全的印表機服務。印表機需要在列印之前設定文件和品質屬性。顯然,如果沒有適當的保護,我們可能會遇到競爭條件。呼叫者不希望一直阻塞直到印表機可用,而 actors 的 fire-and-forget
性質可以非常優雅地解決這個問題。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import groovyx.gpars.agent.Agent
/**
* A non-thread-safe service that slowly prints documents on at a time
*/
class PrinterService {
String document
String quality
public void printDocument() {
println "Printing $document in $quality quality"
Thread.sleep 5000
println "Done printing $document"
}
}
def printer = new Agent<PrinterService>(new PrinterService())
final Thread thread1 = Thread.start {
for (num in (1..3)) {
final String text = "document $num"
printer << {printerService ->
printerService.document = text
printerService.quality = 'High'
printerService.printDocument()
}
Thread.sleep 200
}
println 'Thread 1 is ready to do something else. All print tasks have been submitted'
}
final Thread thread2 = Thread.start {
for (num in (1..4)) {
final String text = "picture $num"
printer << {printerService ->
printerService.document = text
printerService.quality = 'Medium'
printerService.printDocument()
}
Thread.sleep 500
}
println 'Thread 2 is ready to do something else. All print tasks have been submitted'
}
[thread1, thread2]*.join()
printer.await()
有關最新更新,請參閱各自的 示範 |
讀取值
為了密切遵循 Clojure 的理念,Agent 類別給予讀取比寫入更高的優先權。透過使用 instantVal 屬性,您的讀取請求將會繞過 Agent 的傳入訊息佇列,並傳回目前內部狀態的快照。val 屬性將會在訊息佇列中等待處理,就像非阻塞變體 valAsync(Clojure cl) 一樣,它會使用內部狀態作為參數來調用提供的閉包。
您必須記住,雖然 instantVal 屬性可能會傳回正確的結果,但這些結果看起來是隨機的,因為 instantVal 執行時的 Agent 內部狀態是不確定的,並且取決於執行緒排程器執行 instantVal 主體之前已處理的訊息。
await() 方法可讓您等待提交到 Agent 的所有訊息處理完畢,因此可能會阻塞呼叫執行緒。
狀態複製策略
為了避免洩漏內部狀態,Agent 類別可以指定一個 複製策略
作為第二個建構子參數。指定 複製策略
後,內部狀態會由 複製策略
閉包處理,並且 複製策略
值的輸出值會回傳給呼叫者,而不是實際的內部狀態。這適用於 instantVal、val 以及 valAsync()。
錯誤處理
從提交的命令內拋出的例外狀況會儲存在 agent 內部,並且可以從 errors 屬性取得。該屬性在讀取後會被清除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def clubMembers = new Agent<List>()
assert clubMembers.errors.empty
clubMembers.send {throw new IllegalStateException('test1')}
clubMembers.send {throw new IllegalArgumentException('test2')}
clubMembers.await()
List errors = clubMembers.errors
assert 2 == errors.size()
assert errors[0] instanceof IllegalStateException
assert 'test1' == errors[0].message
assert errors[1] instanceof IllegalArgumentException
assert 'test2' == errors[1].message
assert clubMembers.errors.empty
公平與不公平的 Agents (代理人)
Agent 可以是公平的或不公平的。公平的 agent 在處理每個訊息後會放棄執行緒,而不公平的 agent 會保持執行緒直到其訊息佇列為空。因此,不公平的 agent 往往比公平的 agent 效能更好。
所有 Agent 實例的預設設定都是不公平的,但是透過呼叫其 makeFair() 方法,可以將實例設定為公平的。
1
2
def clubMembers = new Agent<List>(['Me']) //add Me
clubMembers.makeFair()

資料流使用者指南
資料流並行提供了一種替代的並行模型,該模型本質上是安全且穩固的。
簡介
請參考這個使用 GPars 以 Groovy 編寫的小範例,以計算三個並行執行任務的結果總和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import static groovyx.gpars.dataflow.Dataflow.task
final def x = new DataflowVariable()
final def y = new DataflowVariable()
final def z = new DataflowVariable()
task {
z << x.val + y.val
}
task {
x << 10
}
task {
y << 5
}
println "Result: ${z.val}"
使用 Dataflows 類別重寫的相同演算法如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import static groovyx.gpars.dataflow.Dataflow.task
final def df = new Dataflows()
task {
df.z = df.x + df.y
}
task {
df.x = 10
}
task {
df.y = 5
}
println "Result: ${df.z}"
我們啟動三個邏輯任務,它們可以並行執行並執行其特定活動。這些任務需要交換資料,它們使用 資料流變數
來進行交換。將 資料流變數
視為一次性通道,安全可靠地將資料從生產者傳輸到消費者。
資料流變數
具有非常直接的語義。當任務需要從 DataflowVariable
讀取值時(透過 val 屬性),它將會阻塞,直到值被另一個任務或執行緒設定(使用 '<<' 運算子)。每個 資料流變數
在其生命週期中只能設定一次。
請注意,您不必費心排序和同步任務或執行緒及其對共享變數的存取。這些值會在正確的時間神奇地在任務之間傳輸,而無需您的干預。資料會在任務/執行緒之間無縫流動,而無需您的干預或關心。
實作細節
範例中的三個任務不一定需要對應到三個實體執行緒。任務表示所謂的「綠色」或「邏輯」執行緒,並且可以在底層對應到任意數量的實體執行緒。實際的對應取決於排程器,但是資料流演算法的結果並不取決於實際的排程。
概念
資料流程式設計
引用維基百科
(在 資料流
程式中)操作由具有輸入和輸出的「黑盒子」組成,所有這些輸入和輸出始終都明確定義。它們會在所有輸入都變為有效時執行,而不是在程式遇到它們時執行。傳統的程式本質上由一系列陳述組成,表示「執行此操作,現在執行此操作」,而 資料流
程式更像是一系列生產線上的工人,他們會在材料到達時執行其分配的任務。
這就是為什麼資料流語言本質上是並行的:操作沒有隱藏的狀態需要追蹤,並且操作都同時「準備就緒」。
原則
透過 資料流並行
,您可以安全地跨任務共享變數。這些變數(在 Groovy 中是 DataflowVariable
類別的實例)在其生命週期中只能被指定(使用 '<<' 運算子)一次值。另一方面,變數的值可以多次讀取(在 Groovy 中透過 val
屬性),甚至在值被指定之前。在這種情況下,讀取任務會暫停,直到值被另一個任務設定。因此,您可以簡單地使用 資料流變數
循序撰寫每個任務的程式碼,而底層機制將確保您以執行緒安全的方式取得所需的所有值。
簡而言之,您通常使用 資料流變數
執行三個操作
-
建立一個
資料流變數
-
等待變數被綁定(讀取它)
-
綁定變數(寫入它)
而這些是您的程式必須遵循的三個基本規則
-
當程式遇到未綁定的變數時,它會等待值。
-
一旦綁定,就無法更改資料流變數的值。
-
資料流變數
可以輕鬆建立並行串流代理程式。
資料流佇列與廣播
在您查看我們的 資料流變數、任務 和 運算子 範例之前,您應該先了解一些關於串流和佇列的知識,以全面了解 資料流並行
。除了 資料流變數
之外,還有 DataflowQueues 和 DataflowBroadcast 的概念,您可以在程式碼中加以利用。
您可以將它們視為用於並行任務或執行緒之間訊息傳輸的執行緒安全緩衝區或佇列。請查看典型的生產者-消費者示範
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import static groovyx.gpars.dataflow.Dataflow.task
def words = ['Groovy', 'fantastic', 'concurrency', 'fun', 'enjoy', 'safe', 'GPars', 'data', 'flow']
final def buffer = new DataflowQueue()
task {
for (word in words) {
buffer << word.toUpperCase() //add to the buffer
}
}
task {
while(true) println buffer.val //read from the buffer in a loop
}
DataflowBroadcasts 和 DataflowQueues 都像 資料流變數
一樣,實作了 DataflowChannel
介面,具有允許我們寫入它們和從它們讀取值的通用方法。
一旦您開始使用它們來連接 任務、運算子 或 選擇器,透過 DataflowChannel 介面以相同方式處理這兩種型別的能力就變得非常方便。
請參考 API 文件以取得有關通道介面的更多詳細資訊。
點對點通訊
DataflowQueue 類別可以被視為點對點(1 對 1,多對 1)通訊通道。它允許多個生產者向一個讀取器傳送訊息。如果多個讀取器從同一個 DataflowQueue 讀取,它們將各自消耗不同的訊息。
或者換句話說,每個訊息都只會被一個讀取器消耗。您可以輕鬆想像一個圍繞著共享 DataflowQueue 建立的簡單負載平衡方案,當演算法的消費者部分需要擴展時,讀取器會動態新增。這也是連接任務或運算子時的一個有用的預設選擇。
發布-訂閱通訊
DataflowBroadcast 類別提供了一個發布-訂閱(1 對多,多對多)通訊模型。一個或多個生產者寫入訊息,而所有註冊的讀取器都將收到所有訊息。因此,每個訊息都會被在訊息寫入通道時具有有效訂閱的所有讀取器消耗。讀取器透過呼叫 createReadChannel() 方法來訂閱。
1
2
3
4
5
6
7
8
9
10
11
DataflowWriteChannel broadcastStream = new DataflowBroadcast()
DataflowReadChannel stream1 = broadcastStream.createReadChannel()
DataflowReadChannel stream2 = broadcastStream.createReadChannel()
broadcastStream << 'Message1'
broadcastStream << 'Message2'
broadcastStream << 'Message3'
assert stream1.val == stream2.val
assert stream1.val == stream2.val
assert stream1.val == stream2.val
在底層,DataflowBroadcast 使用 DataflowStream 類別來實作訊息傳遞。
DataflowStream
DataflowStream 類別代表一個確定性的資料流通道。它圍繞著功能佇列的概念構建,因此為訊息傳遞提供了無鎖定執行緒安全實作。
本質上,您可以將 DataflowStream 機制視為 1 對多通訊通道,因為當讀取器消耗訊息時,其他讀取器仍然可以讀取相同的訊息。此外,所有訊息都會以相同的順序到達所有讀取器。
由於 DataflowStream 是作為功能佇列實作的,因此其 API 要求使用者自行遍歷串流中的值。另一方面,DataflowStream 提供了方便的方法來進行值篩選或轉換,以及有趣的效能特性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import groovyx.gpars.dataflow.stream.DataflowStream
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.scheduler.ResizeablePool
/**
* Demonstrates concurrent implementation of the Sieve of Eratosthenes using dataflow tasks
*
* In principle, the algorithm consists of a concurrently run chained filters,
* each of which detects whether the current number can be divided by a single prime number.
* (generate nums 1, 2, 3, 4, 5, ...) -> (filter by mod 2) -> (filter by mod 3) -> (filter by mod 5) -> (filter by mod 7) -> (filter by mod 11) -> (caution! Primes falling out here)
* The chain is built (grows) on the fly, whenever a new prime is found
*/
/**
* We need a resizeable thread pool, since tasks consume threads while waiting, blocked for values from the DataflowQueue.val
*/
group = new DefaultPGroup(new ResizeablePool(true))
final int requestedPrimeNumberCount = 100
/**
* Generating candidate numbers
*/
final DataflowStream candidates = new DataflowStream()
group.task {
candidates.generate(2, {it + 1}, {it < 1000})
}
/**
* Chain a new filter for a particular prime number to the end of the Sieve
* @param inChannel The current end channel to consume
* @param prime The prime number to divide future prime candidates with
* @return A new channel ending the whole chain
*/
def filter(DataflowStream inChannel, int prime) {
inChannel.filter { number ->
group.task {
number % prime != 0
}
}
}
/**
* Consume Sieve output and add additional filters for all found primes
*/
def currentOutput = candidates
requestedPrimeNumberCount.times {
int prime = currentOutput.first
println "Found: $prime"
currentOutput = filter(currentOutput, prime)
}
為了方便起見,並且為了能夠將 DataflowStream 物件與其他資料流結構(例如運算子)一起使用,您可以使用 DataflowReadAdapter 包裝它以進行讀取存取,或使用 DataflowWriteAdapter 包裝它以進行寫入存取。
DataflowStream 類別專為單執行緒生產者和消費者設計。如果多個執行緒應讀取或將值寫入串流,則必須在外部序列化它們對串流的存取,或應使用配接器。
DataflowStream 配接器
DataflowStream API 以及其使用語義與 Dataflow(Read/Write)Channel 定義的非常不同。必須使用配接器才能使 DataflowStream 與其他資料流元素一起使用。DataflowStreamReadAdapter 類別將使用讀取值所需的方法包裝 DataflowStream,而 DataflowStreamWriteAdapter 類別則提供圍繞包裝 DataflowStream 方法的寫入方法。
執行緒安全性
重要的是要提到 DataflowStreamWriteAdapter 是執行緒安全的。它允許多個執行緒透過配接器將值新增到包裝的 DataflowStream。另一方面,DataflowStreamReadAdapter 則設計為由單一執行緒使用。
DataflowStreamWriteAdapter 是執行緒安全的 |
為了盡量減少額外負荷並與 DataflowStream 語義保持一致,DataflowStreamReadAdapter 類別不是執行緒安全的,應僅在單一執行緒中使用。
如果多個執行緒需要從 DataflowStream 讀取,它們應建立自己的 DataflowStreamReadAdapter 包裝。
由於有了配接器,DataflowStream 可用於運算子或選擇器之間的通訊,因為這些運算子或選擇器需要 Dataflow(Read/Write)Channels。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.dataflow.stream.DataflowStream
import groovyx.gpars.dataflow.stream.DataflowStreamReadAdapter
import groovyx.gpars.dataflow.stream.DataflowStreamWriteAdapter
import static groovyx.gpars.dataflow.Dataflow.selector
import static groovyx.gpars.dataflow.Dataflow.operator
/**
* Demonstrates the use of DataflowStreamAdapters to allow dataflow operators to use DataflowStreams
*/
final DataflowStream a = new DataflowStream()
final DataflowStream b = new DataflowStream()
def aw = new DataflowStreamWriteAdapter(a)
def bw = new DataflowStreamWriteAdapter(b)
def ar = new DataflowStreamReadAdapter(a)
def br = new DataflowStreamReadAdapter(b)
def result = new DataflowQueue()
def op1 = operator(ar, bw) {
bindOutput it
}
def op2 = selector([br], [result]) {
result << it
}
aw << 1
aw << 2
aw << 3
assert([1, 2, 3] == [result.val, result.val, result.val])
op1.stop()
op2.stop()
op1.join()
op2.join()
此外,從多個 DataflowChannels 選擇值的能力只能透過 DataflowStream 的配接器使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import groovyx.gpars.dataflow.Select
import groovyx.gpars.dataflow.stream.DataflowStream
import groovyx.gpars.dataflow.stream.DataflowStreamReadAdapter
import groovyx.gpars.dataflow.stream.DataflowStreamWriteAdapter
import static groovyx.gpars.dataflow.Dataflow.select
import static groovyx.gpars.dataflow.Dataflow.task
/**
* Demonstrates the use of DataflowStreamAdapters to allow dataflow select to select on DataflowStreams
*/
final DataflowStream a = new DataflowStream()
final DataflowStream b = new DataflowStream()
def aw = new DataflowStreamWriteAdapter(a)
def bw = new DataflowStreamWriteAdapter(b)
def ar = new DataflowStreamReadAdapter(a)
def br = new DataflowStreamReadAdapter(b)
final Select<?> select = select(ar, br)
task {
aw << 1
aw << 2
aw << 3
}
assert 1 == select().value
assert 2 == select().value
assert 3 == select().value
task {
bw << 4
aw << 5
bw << 6
}
def result = (1..3).collect{select()}.sort{it.value}
assert result*.value == [4, 5, 6]
assert result*.index == [1, 0, 1]
如果您不需要任何功能佇列 DataflowStream 的特殊功能,例如產生、篩選或對應,則可以考慮改用 DataflowBroadcast 類別。
這個類別透過 DataflowChannel 介面提供發布-訂閱 通訊模型。
綁定處理器
1
2
3
4
5
6
def a = new DataflowVariable()
a >> {println "The variable has just been bound to $it"}
a.whenBound {println "Just to confirm that the variable has been really set to $it"}
...
綁定處理器
可以使用 '>>' 運算子和/或 then() 或 whenBound() 方法註冊到所有資料流通道(變數、佇列或廣播)。它們僅在值綁定到變數後才會執行。
資料流佇列
和 廣播
也支援 wheneverBound 方法,用於註冊一個閉包或訊息處理器,以便在每次將值綁定到它們時執行。
1
2
def queue = new DataflowQueue()
queue.wheneverBound {println "A value $it arrived to the queue"}
顯然,您可以針對單一的 Promise 擁有一個以上的處理器:一旦 promise 有了具體的值,它們將會並行觸發。
wheneverBound
範例
1
2
3
4
5
6
7
8
Promise bookingPromise = task {
final data = collectData()
return broker.makeBooking(data)
}
bookingPromise.whenBound {booking -> printAgenda booking}
bookingPromise.whenBound {booking -> sendMeAnEmailTo booking}
bookingPromise.whenBound {booking -> updateTheCalendar booking}
綁定處理器分組
當您需要等待多個 DataflowVariables Promises
被綁定時,我們可以利用呼叫 whenAllBound() 函數。它在 Dataflow 類別以及 PGroup 實例上都可用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
final group = new NonDaemonPGroup()
//Calling asynchronous services and receiving back promises for the reservations
Promise flightReservation = flightBookingService('PAR <-> BRU')
Promise hotelReservation = hotelBookingService('BRU:Feb 24 20015 - Feb 29 2015')
Promise taxiReservation = taxiBookingService('BRU:Feb 24 2015 10:31')
//when all reservations have been made, we need to build an agenda for our trip
Promise agenda = group.whenAllBound(flightReservation, hotelReservation, taxiReservation) {flight, hotel, taxi ->
"Agenda: $flight | $hotel | $taxi"
}
//since this is a demo, we only print the agenda and block when it's ready
println agenda.val
如果您不知道 whenAllBound() 處理器需要多少參數,則可以使用具有一個 List 類型參數的閉包。
1
2
3
4
5
6
7
8
9
10
11
Promise module1 = task {
compile(module1Sources)
}
Promise module2 = task {
compile(module2Sources)
}
//We don't know the number of modules that will be jarred together, so use a List
final jarCompiledModules = {List modules -> ...}
whenAllBound([module1, module2], jarCompiledModules)
綁定處理器鏈結
所有資料流通道也支援 then() 方法,用於註冊一個回呼處理器,以便在值可用時調用。 與 whenBound() 不同,then() 方法允許我們使用鏈接,讓我們可以在函數之間異步傳輸結果值。
Groovy 允許我們在 then() 方法鏈中省略一些點。 |
1
2
3
4
5
6
final DataflowVariable variable = new DataflowVariable()
final DataflowVariable result = new DataflowVariable()
variable.then {it * 2} then {it + 1} then {result << it}
variable << 4
assert 9 == result.val
1
2
3
4
5
6
7
8
9
10
11
final DataflowVariable variable = new DataflowVariable()
final DataflowVariable result = new DataflowVariable()
final doubler = {it * 2}
final adder = {it + 1}
variable.then doubler then adder then {result << it}
Thread.start {variable << 4}
assert 9 == result.val
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ActiveObject
class ActiveDemoCalculator {
@ActiveMethod
def doubler(int value) {
value * 2
}
@ActiveMethod
def adder(int value) {
value + 1
}
}
final DataflowVariable result = new DataflowVariable()
final calculator = new ActiveDemoCalculator();
calculator.doubler(4).then {calculator.adder it}.then {result << it}
assert 9 == result.val
1
2
3
4
variable.whenBound {value ->
Promise promise = asyncFunction(value)
println promise.get()
}
或者,它可以註冊另一個(巢狀)whenBound() 處理器,這會導致程式碼不必要地複雜。
1
2
3
4
5
variable.whenBound {value ->
asyncFunction(value).whenBound {
println it
}
}
為了說明,請比較以下兩個程式碼片段。 一個使用 whenBound(),另一個使用 then() 鏈接。 它們在功能和行為方面是等效的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final DataflowVariable variable = new DataflowVariable()
final doubler = {it * 2}
final inc = {it + 1}
//Using whenBound()
variable.whenBound {value ->
task {
doubler(value)
}.whenBound {doubledValue ->
task {
inc(doubledValue)
}.whenBound {incrementedValue ->
println incrementedValue
}
}
}
//Using then() chaining
variable.then doubler then inc then this.&println
Thread.start {variable << 4}
1
variable >> asyncFunction >> {println it}
RightShift '>>' 運算子已超載以呼叫 then() 方法,因此可以以相同的方式鏈接。
1
2
3
4
5
6
7
8
9
10
11
final DataflowVariable variable = new DataflowVariable()
final DataflowVariable result = new DataflowVariable()
final doubler = {it * 2}
final adder = {it + 1}
variable >> doubler >> adder >> {result << it}
Thread.start {variable << 4}
assert 9 == result.val
Promise 鏈接的錯誤處理
異步操作可能會拋出例外。 能夠輕鬆且輕鬆地處理它們非常重要。 GPars Promise 物件可以隱式地將異常從異步計算傳播到整個 promise 鏈。
-
Promises 會傳播結果值以及例外。 阻塞的 get() 方法會重新拋出綁定到 Promise 的任何例外,以便呼叫者可以處理它。
-
對於
異步通知
- whenBound() 處理器閉包 - 會將例外作為參數傳遞進來。 -
then() 方法接受兩個參數 - 一個 值處理器 和一個可選的 錯誤處理器。 這些將根據結果是常規值還是例外而調用。 如果未指定 errorHandle,則例外會重新拋出到 then() 返回的 Promise 。
-
對於偵聽多個 Promises 以取得綁定的 whenAllBound() 方法,then() 方法的行為完全相同。
1
2
3
4
5
6
7
8
9
10
Promise<Integer> initial = new DataflowVariable<Integer>()
Promise<String> result = initial.then {it * 2} then {100 / it} // Will throw exception for 0
.then {println "Log the value $it as it passes by"; return it} // No error handler is defined,
// so exceptions are ignored
// and silently re-thrown to the next handler in chain
.then({"The result for $num is $it"}, {"Error detected for $num: $it"}) // Here the exception is caught
initial << 0
println result.get()
ErrorHandler 是一個閉包,它接受 Throwable 的實例作為其唯一(可選)參數。 它返回一個應該綁定到 then() 方法呼叫結果的值,即返回的 Promise。 如果從錯誤處理器內拋出例外,則會將其作為錯誤綁定到產生的 Promise。
1
2
3
4
5
promise.then({it+1}) // Implicitly re-throws potential exceptions bound to promise
promise.then({it+1}, {e -> throw e}) // Explicitly re-throws potential exceptions bound to promise
promise.then({it+1}, {e -> throw new RuntimeException('Error occurred', e})
// Explicitly re-throws a new exception wrapping a potential exception bound to a *Promise*
您希望在哪裡處理這個例外?
Java 中的例外處理具有 try-catch 語句。 GPars Promise 物件的行為讓異步調用可以自由地在最方便的任何位置處理例外。 如果您願意,可以自由地忽略程式碼中的例外,然後假設一切正常。 即使如此,請記住,例外不會意外地被吞噬。
1
2
3
4
5
6
7
task {
'gpars.org'.toURL().text //should throw MalformedURLException
}
.then {page -> page.toUpperCase()}
.then {page -> page.contains('GROOVY')}
.then({mentionsGroovy -> println "Groovy found: $mentionsGroovy"}, {error -> println "Error: $error"}).join()
處理具體例外類型
您也可以更具體地了解處理的例外類型,如下所示
1
2
3
4
5
url.then(download)
.then(calculateHash, {MalformedURLException e -> return 0}) // <- specific !
.then(formatResult)
.then(printResult, printError)
.then(sendNotificationEmail);
客戶端例外處理
您可能希望完全不處理例外,然後讓客戶端(消費者)處理它
1
2
3
4
5
6
Promise<Object> result = url.then(download).then(calculateHash).then(formatResult).then(printResult);
try {
result.get()
} catch (Exception e) {
//handle exceptions here
}
將它們整合在一起
透過結合 whenAllBound() 和 then(或 '>>')方法,我們可以輕鬆地以便利的方式管理大型異步場景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
withPool {
Closure download = {String url ->
sleep 3000 //Simulate a web read
'web content'
}.asyncFun()
Closure loadFile = {String fileName ->
'file content' //simulate a local file read
}.asyncFun()
Closure hash = {s -> s.hashCode()}
Closure compare = {int first, int second ->
first == second
}
Closure errorHandler = {println "Error detected: $it"}
def all = whenAllBound([
download('http://www.gpars.org') >> hash,
loadFile('/coolStuff/gpars/website/index.html') >> hash
], compare).then({println it}, errorHandler)
all.join() //optionally block until the calculation is all done
請注意,只有初始動作(函數)需要是異步的。管道中更下游的函數將由您的 Promise 異步調用,即使它們是同步的。
使用 Promises 實現 Fork/join 模式
Promises 非常靈活,可以用作許多不同場景的實作工具。 這是一個 Promise 的另一個方便的額外功能。
一旦目前 Promise 被綁定,_thenForkAndJoin() 方法會觸發一個或多個活動,並返回一個已完成的 Promise 物件,該物件僅在所有活動完成後才被綁定。
讓我們看看它如何融入整個流程
-
then() - 允許活動鏈接,以便一個活動在另一個活動之後執行
-
whenAllBound() - 允許結合多個活動; 只有在所有活動完成後才會開始新的活動
-
task() - 允許我們建立(fork)多個異步活動
-
thenForkAndJoin() - 用於 fork 多個活動並結合它們的簡寫語法
因此,透過 thenForkAndJoin(),您可以簡單地建立多個應由共用(觸發)Promise 觸發的活動。
1
promise.thenForkAndJoin(task1, task2, task3).then{...}
一旦所有活動都返回結果,它們就會被收集到一個列表中並綁定到 thenForkAndJoin() 返回的 Promise 中。
1
2
3
task {
2
}.thenForkAndJoin({ it ** 2 }, { it**3 }, { it**4 }, { it**5 }).then({ println it}).join()
惰性的 Dataflow 任務與變數
有時,您可能需要將 資料流變數
的品質與延遲初始化結合使用。
1
2
3
4
5
6
Closure<String> download = {url ->
println "Downloading"
url.toURL().text
}
def pageContent = new LazyDataflowVariable(download.curry("https://gpars.dev.org.tw"))
LazyDataflowVariable 的實例在建構時宣告一個初始化器。 只有當有人透過阻塞的 get() 方法或使用任何非阻塞的回呼方法(例如 then())要求其值時,才會觸發實例。由於 LazyDataflowVariables 保留了普通 DataflowVariables 的所有優點,您可以輕鬆地將它們與其他 lazy 或 ordinary 資料流變數
鏈接在一起。
一個更大的範例
此討論值得一個更實際的範例。 因此,從 這篇長文 中獲得靈感,以下程式碼片段示範如何使用 LazyDataflowVariables 來延遲且異步地將相互依賴的元件載入記憶體中。元件模組將按照它們的依賴順序載入,如果可能的話,會同時載入。
每個模組只會載入一次,而不論依賴它的模組數量有多少。 由於 延遲
,只有過渡性需要的模組才會被載入。 我們的範例使用一個簡單的「菱形」依賴方案
-
D 依賴 B 和 C
-
C 依賴 A
-
B 依賴 A
載入 D 時,A 會先被載入。一旦 A 被載入,B 和 C 將會同時載入。 一旦 B 和 C 都被載入,D 將會開始載入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def moduleA = new LazyDataflowVariable({->
println "Loading moduleA into memory"
sleep 3000
println "Loaded moduleA into memory"
return "moduleA"
})
def moduleB = new LazyDataflowVariable({->
moduleA.then {
println "->Loading moduleB into memory, since moduleA is ready"
sleep 3000
println " Loaded moduleB into memory"
return "moduleB"
}
})
def moduleC = new LazyDataflowVariable({->
moduleA.then {
println "->Loading moduleC into memory, since moduleA is ready"
sleep 3000
println " Loaded moduleC into memory"
return "moduleC"
}
})
def moduleD = new LazyDataflowVariable({->
whenAllBound(moduleB, moduleC) { b, c ->
println "-->Loading moduleD into memory, since moduleB and moduleC are ready"
sleep 3000
println " Loaded moduleD into memory"
return "moduleD"
}
})
println "Nothing loaded so far"
println "==================================================================="
println "Load module: " + moduleD.get()
println "==================================================================="
println "All requested modules loaded"
使任務延遲
lazyTask() 方法與 task() 方法一起使用,為我們提供延遲活動的面向任務抽象。 Lazy Task 返回一個 LazyDataflowVariable 的實例(如 Promise),其初始化器由提供的閉包設定。 一旦有人要求該值,任務將會異步啟動,並最終將值傳遞到 LazyDataflowVariable 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
import groovyx.gpars.dataflow.Dataflow
def pageContent = Dataflow.lazyTask {
println "Downloading"
"https://gpars.dev.org.tw".toURL().text
}
println "No-one has asked for the value just yet. Bound = ${pageContent.bound}"
sleep 1000
println "Now going to ask for a value"
println pageContent.get().size()
println "Repetitive requests will receive the already calculated value. No additional downloading."
println pageContent.get().size()
資料流表達式
看看下面的魔力
1
2
3
4
5
6
7
8
9
10
11
12
def initialDistance = new DataflowVariable()
def acceleration = new DataflowVariable()
def time = new DataflowVariable()
task {
initialDistance << 100
acceleration << 2
time << 10
}
def result = initialDistance + acceleration*0.5*time**2
println 'Total distance ' + result.val
我們使用 DataflowVariables 來表示計算加速物體總距離的數學方程式的幾個參數。 然而,在方程式本身中,我們直接使用 DataflowVariable。 我們沒有參考它們所代表的值,但我們仍然能夠正確地進行數學運算。 這表明 DataflowVariables 可以非常靈活。
例如,您可以呼叫它們的方法,這些方法會分派到綁定的值
1
2
3
4
5
def name = new DataflowVariable()
task {
name << ' adam '
}
println name.toUpperCase().trim().val
您可以將其他 DataflowVariables 作為參數傳遞給這些方法,並且會自動傳遞實際值
1
2
3
4
5
6
7
8
9
10
11
def title = new DataflowVariable()
def searchPhrase = new DataflowVariable()
task {
title << ' Groovy in Action 2nd edition '
}
task {
searchPhrase << '2nd'
}
println title.trim().contains(searchPhrase).val
您也可以直接使用 DataflowVariable 查詢綁定值的屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def book = new DataflowVariable()
def searchPhrase = new DataflowVariable()
task {
book << [
title:'Groovy in Action 2nd edition ',
author:'Dierk Koenig',
publisher:'Manning']
}
task {
searchPhrase << '2nd'
}
book.title.trim().contains(searchPhrase).whenBound {println it} //Asynchronous waiting
println book.title.trim().contains(searchPhrase).val //Synchronous waiting
請注意,結果仍然是一個 DataflowVariable(準確地說是 DataflowExpression
),您可以從中同步或異步地取得實際值。
綁定錯誤通知
DataflowVariables 提供了在綁定操作失敗時向已註冊的監聽器發送通知的功能。getBindErrorManager() 方法允許新增和移除監聽器。當嘗試綁定值失敗(透過 bind()
、bindSafely()
、bindUnique()
或 leftShift()
)或發生錯誤(透過 bindError())時,會通知監聽器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final DataflowVariable variable = new DataflowVariable()
variable.getBindErrorManager().addBindErrorListener(new BindErrorListener() {
@Override
void onBindError(final Object oldValue, final Object failedValue, final boolean uniqueBind) {
println "Bind failed!"
}
@Override
void onBindError(final Object oldValue, final Throwable failedError) {
println "Binding an error failed!"
}
@Override
public void onBindError(final Throwable oldError, final Object failedValue, final boolean uniqueBind) {
println "Bind failed!"
}
@Override
public void onBindError(final Throwable oldError, final Throwable failedError) {
println "Binding an error failed!"
}
})
這讓我們可以自訂對任何嘗試綁定已綁定 Dataflow Variable 的回應。例如,使用 bindSafely(),您不會將綁定異常返回給呼叫者,而是通知已註冊的 BindErrorListener。
任務
Dataflow Task 為我們提供了一個易於理解的抽象概念,表示相互獨立的邏輯任務或執行緒。這些任務或執行緒可以同時執行,並且僅透過 Dataflow Variables
、Queues
、Broadcasts
和 Streams
來交換資料。具有易於表達的相互依賴性和固有順序主體的 Dataflow Task 也可以用作 UML *活動圖* 的實際實現。
查看範例。
一個簡單的混搭範例
在這個範例中,我們在各自的任務中下載三個熱門網站的首頁,同時在另一個獨立的任務中,我們篩選出今天談論 Groovy 的網站並形成輸出。輸出任務透過三個 Dataflow 變數自動與三個下載任務同步,網頁內容透過這三個變數傳遞到輸出任務。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import static groovyx.gpars.GParsPool.withPool
import groovyx.gpars.dataflow.DataflowVariable
import static groovyx.gpars.dataflow.Dataflow.task
/**
* A simple mashup sample, downloads content of three websites
* and checks how many of them refer to Groovy.
*/
def dzone = new DataflowVariable()
def jroller = new DataflowVariable()
def theserverside = new DataflowVariable()
task {
println 'Started downloading from DZone'
dzone << 'http://www.dzone.com'.toURL().text
println 'Done downloading from DZone'
}
task {
println 'Started downloading from JRoller'
jroller << 'http://www.jroller.com'.toURL().text
println 'Done downloading from JRoller'
}
task {
println 'Started downloading from TheServerSide'
theserverside << 'http://www.theserverside.com'.toURL().text
println 'Done downloading from TheServerSide'
}
task {
withPool {
println "Number of Groovy sites today: " +
([dzone, jroller, theserverside].findAllParallel {
it.val.toUpperCase().contains 'GROOVY'
}).size()
}
}.join()
任務分組
Dataflow 任務可以組織成組,以便進行效能微調。組提供了一個方便的 task() 工廠方法,用於建立附加到這些組的任務。使用組可以讓我們圍繞不同的執行緒池(包裹在組內)組織任務或運算子。雖然 Dataflow.task() 命令會在預設執行緒池 (java.util.concurrent.Executor, 固定大小 = #cpu + 1, 精靈執行緒)
上排程任務,但我們可能更喜歡定義自己的執行緒池來執行這些任務。
1
2
3
4
5
6
7
8
9
10
11
12
13
import groovyx.gpars.group.DefaultPGroup
def group = new DefaultPGroup()
group.with {
task {
...
}
task {
...
}
}
我們可以使用 Dataflow.usingGroup() 方法,在程式碼區塊中選擇性地覆寫用於任務、運算子、回呼和其他 dataflow 元素的預設群組
1
2
3
4
5
6
7
8
Dataflow.usingGroup(group) {
task {
'http://gpars.codehaus.org'.toURL().text //should throw MalformedURLException
}
.then {page -> page.toUpperCase()}
.then {page -> page.contains('GROOVY')}
.then({mentionsGroovy -> println "Groovy found: $mentionsGroovy"}, {error -> println "Error: $error"}).join()
}
您可以透過指定特定群組來隨時覆寫預設群組
1
2
3
4
5
6
7
8
Dataflow.usingGroup(group) {
anotherGroup.task {
'http://gpars.codehaus.org'.toURL().text //should throw MalformedURLException
}
.then(anotherGroup) {page -> page.toUpperCase()}
.then(anotherGroup) {page -> page.contains('GROOVY')}.then(anotherGroup) {println Dataflow.retrieveCurrentDFPGroup();it}
.then(anotherGroup, {mentionsGroovy -> println "Groovy found: $mentionsGroovy"}, {error -> println "Error: $error"}).join()
}
具有方法的混搭變體
為了避免給您關於 Dataflow 程式碼結構的錯誤印象,以下是混搭範例的重寫版本,其中 downloadPage() 方法在單獨的任務中執行實際下載。它會傳回 DataflowVariable 實例,以便主應用程式執行緒最終可以取得下載的內容。
Dataflow 變數顯然可以作為參數或傳回值傳遞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package groovyx.gpars.samples.dataflow
import static groovyx.gpars.GParsExecutorsPool.withPool
import groovyx.gpars.dataflow.DataflowVariable
import static groovyx.gpars.dataflow.Dataflow.task
/**
* A simple mashup sample, downloads content of three websites and checks how many of them refer to Groovy.
*/
final List urls = ['http://www.dzone.com', 'http://www.jroller.com', 'http://www.theserverside.com']
task {
def pages = urls.collect { downloadPage(it) }
withPool {
println "Number of Groovy sites today: " +
(pages.findAllParallel {
it.val.toUpperCase().contains 'GROOVY'
}).size()
}
}.join()
def downloadPage(def url) {
def page = new DataflowVariable()
task {
println "Started downloading from $url"
page << url.toURL().text
println "Done downloading from $url"
}
return page
}
一個物理計算範例
Dataflow 程式會隨著處理器的數量自然擴展。在一定程度上,您擁有的處理器越多,程式執行速度就越快。例如,請查看以下腳本,該腳本計算簡單物理實驗的參數並印出結果。
每個任務都會執行其計算部分,並且可能依賴於某些其他任務計算的值,並且某些其他任務可能需要其結果。使用 Dataflow Concurrency
,您可以隨意在任務之間分割工作或重新排序任務本身,而 dataflow 機制會確保正確完成計算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import groovyx.gpars.dataflow.DataflowVariable
import static groovyx.gpars.dataflow.Dataflow.task
final def mass = new DataflowVariable()
final def radius = new DataflowVariable()
final def volume = new DataflowVariable()
final def density = new DataflowVariable()
final def acceleration = new DataflowVariable()
final def time = new DataflowVariable()
final def velocity = new DataflowVariable()
final def decelerationForce = new DataflowVariable()
final def deceleration = new DataflowVariable()
final def distance = new DataflowVariable()
def t = task {
println """
Calculating distance required to stop a moving ball.
....................................................
The ball has a radius of ${radius.val} meters and is made of a material with ${density.val} kg/m3 density,
which means that the ball has a volume of ${volume.val} m3 and a mass of ${mass.val} kg.
The ball has been accelerating with ${acceleration.val} m/s2 from 0 for ${time.val} seconds and so reached a velocity of ${velocity.val} m/s.
Given our ability to push the ball backwards with a force of ${decelerationForce.val} N (Newton), we can cause a deceleration
of ${deceleration.val} m/s2 and so stop the ball at a distance of ${distance.val} m.
...................................................
This example has been calculated asynchronously in multiple tasks using *GPars* Dataflow concurrency in Groovy.
Author: ${author.val}
"""
System.exit 0
}
task {
mass << volume.val * density.val
}
task {
volume << Math.PI * (radius.val ** 3)
}
task {
radius << 2.5
density << 998.2071 //water
acceleration << 9.80665 //free fall
decelerationForce << 900
}
task {
println 'Enter your name:'
def name = new InputStreamReader(System.in).readLine()
author << (name?.trim()?.size()>0 ? name : 'anonymous')
}
task {
time << 10
velocity << acceleration.val * time.val
}
task {
deceleration << decelerationForce.val / mass.val
}
task {
distance << deceleration.val * ((velocity.val/deceleration.val) ** 2) * 0.5
}
t.join()
我盡力確保所有物理計算正確。請隨意變更值,看看您需要多遠的距離才能停止滾動的球。 |
確定性死鎖
如果您碰巧在您的依賴關係中引入死結,則每次執行程式碼時都會發生死結。不允許隨機性。這是 Dataflow Concurrency
的優點之一。無論實際的執行緒排程方案為何,如果您在測試中沒有遇到死結,您在生產環境中也不會遇到。
1
2
3
4
5
6
7
8
9
task {
println a.val
b << 'Hi there'
}
task {
println b.val
a << 'Hello man'
}
Dataflows Map
作為一個方便的快捷方式,Dataflows 類別可以協助您減少利用 Dataflow Variables
所需的程式碼量。
1
2
3
4
5
6
7
8
def df = new Dataflows()
df.x = 'value1'
assert df.x == 'value1'
Dataflow.task {df.y = 'value2}
assert df.y == 'value2'
將 Dataflows 視為一個以 Dataflow Variables
作為鍵,將其綁定值儲存為適當的對應值的映射。讀取值(例如 df.x)和綁定值(例如 df.x = 'value')的語意與純 Dataflow Variables
的語意(分別為 x.val 和 x << 'value')相同。
將 Dataflows 和 Groovy with 區塊混合使用
當位於 Dataflows 實例的 with 區塊內時,可以直接存取儲存在 Dataflows 實例中的 Dataflow Variables
,而無需在它們前面加上 Dataflows 實例識別碼。
1
2
3
4
5
6
7
8
new Dataflows().with {
x = 'value1'
assert x == 'value1'
Dataflow.task {y = 'value2}
assert y == 'value2'
}
從任務回傳值
通常,Dataflow 任務會透過 Dataflow Variables
進行通訊。除此之外,任務也可以透過 Dataflow Variable
傳回值。當您調用 task() 工廠方法時,您會取回一個 Promise 的實例(實作為 DataflowVariable
),透過該實例,您可以監聽任務的傳回值,就像使用任何其他 Promise 或 DataflowVariable
一樣。
1
2
3
4
5
6
7
8
9
10
final Promise t1 = task {
return 10
}
final Promise t2 = task {
return 20
}
def results = [t1, t2]*.val
println 'Both sub-tasks finished and returned values: ' + results
也可以使用 whenBound() 方法,在不阻擋呼叫者的情況下取得值。 |
1
2
3
4
5
def task = task {
println 'The task is running and calculating the return value'
30 // the value to be returned
}
task >> {value -> println "The task finished and returned $value"}
合併任務
透過對任務的結果 Dataflow Variable
使用 join() 操作,您可以阻擋直到任務完成。
1
2
3
4
5
6
7
8
9
10
task {
final Promise t1 = task {
println 'First sub-task running.'
}
final Promise t2 = task {
println 'Second sub-task running'
}
[t1, t2]*.join()
println 'Both sub-tasks finished'
}.join()
選擇
通常,需要從數個 dataflow 通道(例如變數、佇列、廣播或串流)之一取得值。Select 類別適用於這些情況。
Select 可以掃描數個 dataflow 通道,並從所有輸入通道中選取一個通道,其值已準備好讀取。從選取的通道讀取值,並將該值與來源通道的索引一起傳回給呼叫者。選取通道是隨機的,或基於通道優先順序,在這種情況下,Select 建構函式中位置索引較低的通道具有較高的優先順序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.dataflow.DataflowVariable
import static groovyx.gpars.dataflow.Dataflow.select
import static groovyx.gpars.dataflow.Dataflow.task
/**
* Shows a basic use of Select, which monitors a set of input channels for values and makes these values
* available on its output irrespective of their original input channel.
* Note that dataflow variables and queues can be combined for Select.
*
* You might also consider checking out the prioritySelect method, which prioritizes values by the index of their input channel
*/
def a = new DataflowVariable()
def b = new DataflowVariable()
def c = new DataflowQueue()
task {
sleep 3000
a << 10
}
task {
sleep 1000
b << 20
}
task {
sleep 5000
c << 30
}
def select = select([a, b, c])
println "The fastest result is ${select().value}"
有數種方法可以從 Select 讀取值
1
2
3
4
5
6
7
8
def sel = select(a, b, c, d)
def result = sel.select() //Random selection
def result = sel() //Random selection (a short-hand variant)
def result = sel.select([true, true, false, true]) //Random selection with guards specified
def result = sel([true, true, false, true]) //Random selection with guards specified (a short-hand variant)
def result = sel.prioritySelect() //Priority selection
def result = sel.prioritySelect([true, true, false, true]) //Priority selection with guards specifies
預設情況下,Select 方法會阻擋呼叫者的處理,直到有可讀取的值可用為止。替代的 selectToPromise() 和 prioritySelectToPromise() 方法為我們提供了一種取得值的 Promise 的方法,該值稍後可以選取。透過傳回的 Promise,您可以註冊一個回呼,以便在選取下一個值時非同步調用。
1
2
3
4
5
6
7
def sel = select(a, b, c, d)
Promise result = sel.selectToPromise() //Random selection
Promise result = sel.selectToPromise([true, true, false, true]) //Random selection with guards specified
Promise result = sel.prioritySelectToPromise() //Priority selection
Promise result = sel.prioritySelectToPromise([true, true, false, true]) //Priority selection with guards specifies
或者,Select 方法可以將其值傳送到宣告的 MessageStream(例如 Actor),而不會阻擋呼叫者。
1
2
3
4
5
6
7
8
9
10
def handler = actor {...}
def sel = select(a, b, c, d)
sel.select(handler) //Random selection
sel(handler) //Random selection (a short-hand variant)
sel.select(handler, [true, true, false, true]) //Random selection with guards specified
sel(handler, [true, true, false, true]) //Random selection with guards specified (a short-hand variant)
sel.prioritySelect(handler) //Priority selection
sel.prioritySelect(handler, [true, true, false, true]) //Priority selection with guards specifies
防護
Guards 讓呼叫者可以從選取中省略某些輸入通道。Guards 指定為傳遞至 select() 或 prioritySelect() 方法的布林旗標清單。
1
2
def sel = select(leaders, seniors, experts, juniors)
def teamLead = sel([true, true, false, false]).value //Only 'leaders' and 'seniors' qualify for becoming a teamLead here
Guards 的典型用途是使 Selects 具有足夠的彈性,以適應使用者狀態的變更。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.select
import static groovyx.gpars.dataflow.Dataflow.task
/**
* Demonstrates the ability to enable/disable channels during a value selection on a Select by providing boolean guards.
*/
final DataflowQueue operations = new DataflowQueue()
final DataflowQueue numbers = new DataflowQueue()
def t = task {
final def select = select(operations, numbers)
3.times {
def instruction = select([true, false]).value
def num1 = select([false, true]).value
def num2 = select([false, true]).value
final def formula = "$num1 $instruction $num2"
println "$formula = ${new GroovyShell().evaluate(formula)}"
}
}
task {
operations << '+'
operations << '+'
operations << '*'
}
task {
numbers << 10
numbers << 20
numbers << 30
numbers << 40
numbers << 50
numbers << 60
}
t.join()
優先選擇
當某些通道在選取時應優先於其他通道時,應改用 prioritySelect 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Here's a simply usecase for Priority Select. It monitors a set of input channels for values and makes these values
* available on its output irrespective of their original input channel.
*
* Note that dataflow variables, queues and broadcasts can be combined for Select.
*
* Unlike plain select method call, the prioritySelect call gives precedence to input channels with lower index.
* Available messages from high priority channels will be served before messages from lower-priority channels.
* Messages received through a single input channel will have their mutual order preserved.
*
*/
def critical = new DataflowVariable()
def ordinary = new DataflowQueue()
def whoCares = new DataflowQueue()
task {
ordinary << 'All working fine'
whoCares << 'I feel a bit tired'
ordinary << 'We are on target'
}
task {
ordinary << 'I have just started my work. Busy. Will come back later...'
sleep 5000
ordinary << 'I am done for now'
}
task {
whoCares << 'Huh, what is that noise'
ordinary << 'Here I am to do some clean-up work'
whoCares << 'I wonder whether unplugging this cable will eliminate that nasty sound.'
critical << 'The server room runs on UPS!'
whoCares << 'The sound has disappeared'
}
def select = select([critical, ordinary, whoCares])
println 'Starting to monitor our IT department'
sleep 3000
10.times {println "Received: ${select.prioritySelect().value}"}
收集非同步計算的結果
無論它們是 dataflow 任務、作用中物件的方法還是非同步函式,非同步活動始終會傳回 Promise。Promises 實作 SelectableChannel 介面,因此可以與其他 Promises 以及 *讀取通道* 一起傳遞至 selects 以進行選取。
與 Java 的 CompletionService 類似,我們的 GPars Select 方法讓您能夠在每個非同步活動可用時立即取得它們的結果。此外,我們可以使用 Select 來從並行執行的數個計算中取得第一個/最快結果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import groovyx.gpars.dataflow.Promise
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
/**
* Demonstrates the use of dataflow tasks and selects to pick the fastest result of concurrently run calculations.
*/
final group = new DefaultPGroup()
group.with {
Promise p1 = task {
sleep(1000)
10 * 10 + 1
}
Promise p2 = task {
sleep(1000)
5 * 20 + 2
}
Promise p3 = task {
sleep(1000)
1 * 100 + 3
}
final alt = new Select(group, p1, p2, p3)
def result = alt.select()
println "Result: " + result
}
逾時
Select.createTimeout() 方法會在宣告的時間段後建立一個綁定到值的 DataflowVariable。這可以在 Selects 中利用,以便在經過所需的延遲後取消阻擋 (恢復處理),如果沒有其他通道在該時間之前傳遞值。只需將 timeout 通道 作為另一個輸入通道傳遞至 Select 即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import groovyx.gpars.dataflow.Promise
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
/**
* Demonstrates the use of dataflow tasks and selects to pick the fastest result of concurrently run calculations.
*/
final group = new DefaultPGroup()
group.with {
Promise p1 = task {
sleep(1000)
10 * 10 + 1
}
Promise p2 = task {
sleep(1000)
5 * 20 + 2
}
Promise p3 = task {
sleep(1000)
1 * 100 + 3
}
final timeoutChannel = Select.createTimeout(500)
final alt = new Select(group, p1, p2, p3, timeoutChannel)
def result = alt.select()
println "Result: " + result
}
取消
好的,所以我們有了答案。那麼繼續研究其答案的其他任務呢?如果我們需要在找到答案或逾時過期後取消其他任務,那麼最好的方法是設定一個我們的任務定期監控的旗標。
有意地,DataflowVariables 或 Tasks 中沒有內建的取消機制 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import groovyx.gpars.dataflow.Promise
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
import java.util.concurrent.atomic.AtomicBoolean
/**
* Demonstrates the use of dataflow tasks and selects to pick the fastest result of concurrently run calculations.
* It shows a waY to cancel the slower tasks once a result is known
*/
final group = new DefaultPGroup()
final done = new AtomicBoolean()
group.with {
Promise p1 = task {
sleep(1000)
if (done.get()) return
10 * 10 + 1
}
Promise p2 = task {
sleep(1000)
if (done.get()) return
5 * 20 + 2
}
Promise p3 = task {
sleep(1000)
if (done.get()) return
1 * 100 + 3
}
final alt = new Select(group, p1, p2, p3, Select.createTimeout(500))
def result = alt.select()
done.set(true)
println "Result: " + result
}
運算子
Dataflow Operators
和 Selectors
提供了完整的 Dataflow 實作,並具備所有常見的慣例。
概念
完整的 Dataflow Concurrency
建構在連接運算子和選取器的通道概念之上。這些物件會使用透過輸入通道傳入的值,將它們轉換為新值,並將新值輸出到其輸出通道。
Operators 會等待 每個 輸入通道都有值,然後才開始處理它們,但 Selectors 只會等待 任何 輸入通道上第一個可用的值。
1
2
3
4
operator(inputs: [a, b, c], outputs: [d]) {x, y, z ->
...
bindOutput 0, x + y + z
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* CACHE
*
* Caches sites' contents. Accepts requests for url content, outputs the content. Outputs requests for download
* if the site is not in cache yet.
*/
operator(inputs: [urlRequests], outputs: [downloadRequests, sites]) {request ->
if (!request.content) {
println "[Cache] Retrieving ${request.site}"
def content = cache[request.site]
if (content) {
println "[Cache] Found in cache"
bindOutput 1, [site: request.site, word:request.word, content: content]
} else {
def downloads = pendingDownloads[request.site]
if (downloads != null) {
println "[Cache] Awaiting download"
downloads << request
} else {
pendingDownloads[request.site] = []
println "[Cache] Asking for download"
bindOutput 0, request
}
}
} else {
println "[Cache] Caching ${request.site}"
cache[request.site] = request.content
bindOutput 1, request
def downloads = pendingDownloads[request.site]
if (downloads != null) {
for (downloadRequest in downloads) {
println "[Cache] Waking up"
bindOutput 1, [site: downloadRequest.site, word:downloadRequest.word, content: request.content]
}
pendingDownloads.remove(request.site)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
def listener = new DataflowEventAdapter() {
@Override
boolean onException(final DataflowProcessor processor, final Throwable e) {
logChannel << e
return false //Indicate whether to terminate the operator or not
}
}
op = group.operator(inputs: [a, b], outputs: [c], listeners: [listener]) {x, y ->
...
}
運算子的類型
運算子方法有專門的版本用於特定目的
-
operator - 基本的通用運算子
-
selector - 由任何輸入通道上的可用值觸發的運算子
-
prioritySelector - 一個選取器,它優先從低索引輸入通道傳遞訊息,而不是從高索引輸入通道傳遞訊息
-
splitter - 一個單一輸入運算子,將其輸入值複製到其所有輸出通道
將運算子連接在一起
運算子通常會組合到網路中,就像某些運算子會使用其他運算子產生的輸出一樣。
1
2
3
operator(inputs:[a, b], outputs:[c, d]) {...}
splitter(c, [e, f])
selector(inputs:[e, d]: outputs:[]) {...}
您也可以透過運算子本身來引用輸出通道
1
2
3
4
def op1 = operator(inputs:[a, b], outputs:[c, d]) {...}
def sp1 = splitter(op1.outputs[0], [e, f]) //takes the first output of op1
selector(inputs:[sp1.outputs[0], op1.outputs[1]]: outputs:[]) {...} //takes the first output of sp1 and the second output of op1
群組運算子
資料流運算子可以組織成群組,以進行效能微調。群組提供方便的 operator() 工廠方法,來建立附加到群組的任務。
1
2
3
4
5
6
7
8
9
10
import groovyx.gpars.group.DefaultPGroup
def group = new DefaultPGroup()
group.with {
operator(inputs: [a, b, c], outputs: [d]) {x, y, z ->
...
bindOutput 0, x + y + z
}
}
您可以使用 Dataflow.usingGroup() 方法,選擇性地覆寫程式碼區塊內用於任務、運算子、回呼和其他資料流元素的預設群組
1
2
3
4
5
6
Dataflow.usingGroup(group) {
operator(inputs: [a, b, c], outputs: [d]) {x, y, z ->
...
bindOutput 0, x + y + z
}
}
您可以透過指定特定群組來隨時覆寫預設群組
1
2
3
4
5
6
Dataflow.usingGroup(group) {
anotherGroup.operator(inputs: [a, b, c], outputs: [d]) {x, y, z ->
...
bindOutput 0, x + y + z
}
}
建構運算子
運算子的建構屬性,例如 inputs、outputs、stateObject 或 maxForks,一旦運算子建立後就無法修改。當您在最終建立運算子之前,逐步將通道和值收集到清單中時,您可能會發現 groovyx.gpars.dataflow.ProcessingNode 類別很有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import groovyx.gpars.dataflow.Dataflow
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.ProcessingNode.node
/**
* Shows how to build operators using the ProcessingNode class
*/
final DataflowQueue aValues = new DataflowQueue()
final DataflowQueue bValues = new DataflowQueue()
final DataflowQueue results = new DataflowQueue()
//Create a config and gradually set the required properties - channels, code, etc.
def adderConfig = node {valueA, valueB ->
bindOutput valueA + valueB
}
adderConfig.inputs << aValues
adderConfig.inputs << bValues
adderConfig.outputs << results
//Build the operator
final adder = adderConfig.operator(Dataflow.DATA_FLOW_GROUP)
//Now the operator is running and processing the data
aValues << 10
aValues << 20
bValues << 1
bValues << 2
assert [11, 22] == (1..2).collect {
results.val
}
在運算子中保存狀態
雖然運算子通常不需要在後續的調用之間保持狀態,但如果開發人員需要,GPars 允許運算子維護狀態。一個顯而易見的方法是利用 Groovy 閉包功能來封閉其上下文。
1
2
3
4
int counter = 0
operator(inputs: [a], outputs: [b]) {value ->
counter += 1
}
另一種方法,可以讓您避免在運算子定義之外宣告狀態物件,是在建構時將狀態物件作為 stateObject 參數傳遞到運算子中
1
2
3
operator(inputs: [a], outputs: [b], stateObject: [counter: 0]) {value ->
stateObject.counter += 1
}
平行化運算子
預設情況下,運算子的主體一次由單個執行緒處理。雖然這是一個安全的設定,允許以非執行緒安全的方式編寫運算子的主體,但一旦運算子變得「熱門」,並且資料開始在運算子的輸入佇列中累積,您可能會考慮允許多個執行緒同時執行運算子的主體。請記住,在這種情況下,您需要避免或保護共享資源免受多執行緒存取。若要允許多個執行緒同時執行運算子的主體,請在建立運算子時傳遞額外的 maxForks 參數
1
2
3
4
def op = operator(inputs: [a, b, c], outputs: [d, e], maxForks: 2) {x, y, z ->
bindOutput 0, x + y + z
bindOutput 1, x * y * z
}
maxForks 參數的值表示同時執行運算子的最大執行緒數。只允許正數,預設值為 1。
1
2
def group = new DefaultPGroup(10)
group.operator((inputs: [a, b, c], outputs: [d, e], maxForks: 5) {x, y, z -> ...}
預設群組使用可調整大小的執行緒池,因此永遠不會耗盡執行緒。
同步輸出
當透過將 maxForks 的值設定為大於 1 的值來啟用運算子的內部平行化時,重要的是要記住,如果運算子主體中沒有明確或隱含的同步,則可能會發生競爭條件。尤其要記住,寫入多個輸出通道的值,不能保證以相同的順序原子地寫入所有通道
1
2
3
4
5
6
7
8
9
operator(inputs:[inputChannel], outputs:[a, b], maxForks:5) {msg ->
bindOutput 0, msg
bindOutput 1, msg
}
inputChannel << 1
inputChannel << 2
inputChannel << 3
inputChannel << 4
inputChannel << 5
May result in output channels having the values mixed-up something like:
1
2
a -> 1, 3, 2, 4, 5
b -> 2, 1, 3, 5, 4
Explicit synchronization is one way to get correctly bound all output channels and protect operator not-thread local state:
1
2
3
4
5
6
7
8
9
10
def lock = new Object()
operator(inputs:[inputChannel], outputs:[a, b], maxForks:5) {msg ->
doStuffThatIsThreadSafe()
synchronized(lock) {
doSomethingThatMustNotBeAccessedByMultipleThreadsAtTheSameTime()
bindOutput 0, msg
bindOutput 1, 2*msg
}
}
顯然,您需要權衡此處的優缺點,因為同步可能會破壞將 maxForks 設定為大於 1 的值的目的。
若要以一個原子步驟設定所有運算子的輸出通道的值,您也可以考慮呼叫 bindAllOutputsAtomically 方法,傳遞單個值以寫入所有輸出通道,或呼叫 bindAllOutputsAtomically 方法,該方法採用多個值,每個值都將寫入具有相同位置索引的輸出通道。
1
2
3
4
5
operator(inputs:[inputChannel], outputs:[a, b], maxForks:5) {msg ->
doStuffThatIsThreadSafe()
bindAllOutputValuesAtomically msg, 2*msg
}
}
運算子生命週期
資料流運算子和選取器在其生命週期中觸發多個事件,這讓相關方可以取得通知並可能變更運算子的行為。DataflowEventListener 介面提供幾個回呼方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public interface DataflowEventListener {
/**
* Invoked immediately after the operator starts by a pooled thread before the first message is obtained
*
* @param processor The reporting dataflow operator/selector
*/
void afterStart(DataflowProcessor processor);
/**
* Invoked immediately after the operator terminates
*
* @param processor The reporting dataflow operator/selector
*/
void afterStop(DataflowProcessor processor);
/**
* Invoked if an exception occurs.
* If any of the listeners returns true, the operator will terminate.
* Exceptions outside of the operator's body or listeners' messageSentOut() handlers will terminate the operator irrespective of the listeners' votes.
*
* @param processor The reporting dataflow operator/selector
* @param e The thrown exception
* @return True, if the operator should terminate in response to the exception, false otherwise.
*/
boolean onException(DataflowProcessor processor, Throwable e);
/**
* Invoked when a message becomes available in an input channel.
*
* @param processor The reporting dataflow operator/selector
* @param channel The input channel holding the message
* @param index The index of the input channel within the operator
* @param message The incoming message
* @return The original message or a message that should be used instead
*/
Object messageArrived(DataflowProcessor processor, DataflowReadChannel<Object> channel, int index, Object message);
/**
* Invoked when a control message (instances of ControlMessage) becomes available in an input channel.
*
* @param processor The reporting dataflow operator/selector
* @param channel The input channel holding the message
* @param index The index of the input channel within the operator
* @param message The incoming message
* @return The original message or a message that should be used instead
*/
Object controlMessageArrived(DataflowProcessor processor, DataflowReadChannel<Object> channel, int index, Object message);
/**
* Invoked when a message is being bound to an output channel.
*
* @param processor The reporting dataflow operator/selector
* @param channel The output channel to send the message to
* @param index The index of the output channel within the operator
* @param message The message to send
* @return The original message or a message that should be used instead
*/
Object messageSentOut(DataflowProcessor processor, DataflowWriteChannel<Object> channel, int index, Object message);
/**
* Invoked when all messages required to trigger the operator become available in the input channels.
*
* @param processor The reporting dataflow operator/selector
* @param messages The incoming messages
* @return The original list of messages or a modified/new list of messages that should be used instead
*/
List<Object> beforeRun(DataflowProcessor processor, List<Object> messages);
/**
* Invoked when the operator completes a single run
*
* @param processor The reporting dataflow operator/selector
* @param messages The incoming messages that have been processed
*/
void afterRun(DataflowProcessor processor, List<Object> messages);
/**
* Invoked when the fireCustomEvent() method is triggered manually on a dataflow operator/selector
*
* @param processor The reporting dataflow operator/selector
* @param data The custom piece of data provided as part of the event
* @return A value to return from the fireCustomEvent() method to the caller (event initiator)
*/
Object customEvent(DataflowProcessor processor, Object data);
}
預設實作是透過 DataflowEventAdapter 類別提供的。
當異常在運算子內部發生時,接聽器提供了一種處理異常的方式。接聽器通常可以記錄此類異常、通知監管實體、產生替代輸出或執行從情況中恢復所需的任何步驟。如果沒有註冊接聽器,或者任何接聽器傳回 true,則運算子將終止,保留 afterStop() 的合約。在實際運算子主體之外發生的異常,即在觸發主體之前的參數準備階段,或在主體完成後的清理和通道訂閱階段,始終會導致運算子終止。
運算子和選取器上可用的 fireCustomEvent() 方法可用於在運算子的主體與相關接聽器之間來回傳輸
1
2
3
4
5
6
7
8
9
10
11
12
13
final listener = new DataflowEventAdapter() {
@Override
Object customEvent(DataflowProcessor processor, Object data) {
println "Log: Getting quite high on the scale $data"
return 100 //The value to use instead
}
}
op = group.operator(inputs: [a, b], outputs: [c], listeners: [listener]) {x, y ->
final sum = x + y
if (sum > 100) bindOutput(fireCustomEvent(sum)) //Reporting that the sum is too high, binding the lowered value that comes back
else bindOutput sum
}
選擇器
選取器的主體應該是一個使用一個或兩個引數的閉包。
1
2
3
selector (inputs : [a, b, c], outputs : [d, e]) {value ->
....
}
雙引數閉包將取得一個值以及目前正在處理其值的輸入通道的索引。這讓選取器能夠區分來自不同輸入通道的值。
1
2
3
selector (inputs : [a, b, c], outputs : [d, e]) {value, index ->
....
}
優先順序選取器
當需要在輸入通道之間保留優先順序時,應使用 DataflowPrioritySelector。
1
2
3
prioritySelector(inputs : [a, b, c], outputs : [d, e]) {value, index ->
...
}
優先順序選取器始終優先選擇來自位置索引較低的通道的值,而不是來自位置索引較高的通道的值。
聯結選取器
未指定主體閉包的選取器會將所有傳入的值複製到其所有輸出通道。
1
def join = selector (inputs : [programmers, analysis, managers], outputs : [employees, colleagues])
內部平行化
也提供了允許內部選取器平行化的 maxForks 屬性。
1
2
3
selector (inputs : [a, b, c], outputs : [d, e], maxForks : 5) {value ->
....
}
防護
就像 Selects 一樣,Selectors 也允許使用者暫時在選取中包含/排除個別輸入通道。guards 輸入屬性可用於在所有輸入通道上設定初始遮罩,然後在選取器的主體中使用 setGuards 和 setGuard 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.selector
import static groovyx.gpars.dataflow.Dataflow.task
/**
* Demonstrates the ability to enable/disable channels during a value selection on a select by providing boolean guards.
*/
final DataflowQueue operations = new DataflowQueue()
final DataflowQueue numbers = new DataflowQueue()
def instruction
def nums = []
selector(inputs: [operations, numbers], outputs: [], guards: [true, false]) {value, index -> //initial guards is set here
if (index == 0) {
instruction = value
setGuard(0, false) //setGuard() used here
setGuard(1, true)
}
else nums << value
if (nums.size() == 2) {
setGuards([true, false]) //setGuards() used here
final def formula = "${nums[0]} $instruction ${nums[1]}"
println "$formula = ${new GroovyShell().evaluate(formula)}"
nums.clear()
}
}
task {
operations << '+'
operations << '+'
operations << '*'
}
task {
numbers << 10
numbers << 20
numbers << 30
numbers << 40
numbers << 50
numbers << 60
}
關閉資料流網路
關閉資料流處理器(運算子和選取器)的網路有時可能是一項艱鉅的任務,尤其是在您需要一個不會留下任何未處理訊息的通用機制時。
資料流運算子和選取器可以透過三種方式終止
-
在需要終止的所有運算子上呼叫 terminate() 方法
-
傳送泊松訊息
-
設定活動監視器網路,該網路會在所有訊息處理完畢後關閉網路
查看 GPars 提供的詳細資訊。
緊急關閉
您可以對任何運算子/選取器呼叫 terminate() 來立即關閉它。如果您追蹤所有處理器,也許是將它們加入到清單中,則停止網路的最快方法是
1
allMyProcessors*.terminate()
但是,這應該被視為緊急出口,因為無法保證處理的訊息或完成的工作。運算子將簡單地立即終止,留下未完成的工作並放棄輸入通道中的訊息。當然,附加到運算子/選取器的生命週期事件接聽器將會調用它們的 afterStop() 事件處理常式,以便例如釋放資源或將註釋輸出到記錄中。
1
2
3
4
5
6
7
8
9
10
def op1 = operator(inputs: [a, b, c], outputs: [d, e]) {x, y, z -> }
def op2 = selector(inputs: [d], outputs: [f, out]) { }
def op3 = prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }
[op1, op2, op3]*.terminate() //Terminate all operators by calling the terminate() method on them
op1.join()
op2.join()
op3.join()
透過 System.exit() 關閉整個 JVM 顯然會關閉資料流網路,但是,不會調用任何生命週期接聽器。 |
溫和地停止運算子
運算子重複處理傳入的訊息。在不遺失任何訊息風險的情況下,停止運算子的唯一安全時刻是在運算子完成處理訊息並即將在其傳入管道中尋找更多訊息之後。這正是 terminateAfterNextRun() 方法的作用。它會在下一組訊息處理完畢後排程運算子關閉。
未處理的訊息將保留在輸入通道中,這允許您稍後處理它們,也許是使用不同的運算子/選取器,或以其他方式處理它們。使用 terminateAfterNextRun(),您不會遺失任何輸入訊息。當您使用一組運算子/選取器來平衡來自通道的訊息負載時,這可能會特別方便。一旦工作負載減少,可以使用 terminateAfterNextRun() 方法來安全地減少負載平衡運算子的池。
偵測關閉
運算子和選取器為那些需要阻止直到運算子終止的人員提供了方便的 join() 方法。
1
allMyProcessors*.join()
這是等待整個資料流網路關閉的最簡單方法,無論使用哪種關閉方法。
毒藥丸 (PoisonPill)
PoisonPill(毒藥丸)是一個常用術語,指一種使用特殊用途訊息來停止接收該訊息的實體的策略。GPars 提供了 PoisonPill 類別,它具有完全相同的效果,可以用於運算子和選擇器。由於 PoisonPill 是一種 ControlMessage(控制訊息),因此對運算子的主體是不可見的,自訂程式碼不需要以任何方式處理它。DataflowEventListeners(資料流事件監聽器)可以透過 controlMessageArrived() 處理方法對 ControlMessages 做出反應。
1
2
3
4
5
6
7
8
9
10
11
def op1 = operator(inputs: [a, b, c], outputs: [d, e]) {x, y, z -> }
def op2 = selector(inputs: [d], outputs: [f, out]) { }
def op3 = prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }
a << PoisonPill.instance //Send the poisson
op1.join()
op2.join()
op3.join()
在接收到毒藥丸後,運算子會在完成當前計算並確保毒藥已傳送到其所有輸出通道後立即終止。這樣做的目的是讓毒藥可以擴散到連接的運算子。此外,儘管運算子通常會等待所有輸入都有值,但在 PoisonPills 的情況下,只要任何輸入出現 PoisonPill,運算子就會立即終止。從其他通道取得的值將會遺失。如果這些訊息本應被處理,則可以認為是網路設計中的錯誤。它們需要一個適當的值作為對等,而不是 PoisonPill 才能正常處理。
另一方面,選擇器會耐心等待從其所有輸入通道接收到 PoisonPill,然後再將其傳送到輸出通道。這種行為可以防止包含涉及選擇器的回饋迴路的網路使用 PoisonPill 關閉。選擇器永遠不會從來自選擇器後面的通道接收到 PoisonPill。對於此類網路,應使用不同的關閉策略。
立即毒藥丸
特別是為了讓選擇器在接收到毒藥丸後立即關閉,引入了立即毒藥丸的概念。由於正常的非立即毒藥丸僅關閉輸入通道,讓選擇器保持活動狀態,直到至少有一個輸入通道保持開啟,因此立即毒藥丸會立即關閉選擇器。顯然,一旦選擇器讀取到立即毒藥丸,其他選擇器的輸入通道中未處理的訊息將不會被選擇器處理。
使用立即毒藥丸,您可以安全地關閉包含參與回饋迴路的選擇器的網路。
1
2
3
4
5
6
7
def op1 = selector(inputs: [a, b, c], outputs: [d, e]) {value, index -> }
def op2 = selector(inputs: [d], outputs: [f, out]) { }
def op3 = prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }
a << PoisonPill.immediateInstance
[op1, op2, op3]*.join()
帶計數的毒藥
當您向下游運算子網路傳送毒藥丸時,您可能需要知道所有運算子或指定數量的運算子何時停止。CountingPoisonPill(計數毒藥丸)類別正是為此目的而設計的。
1
2
3
4
5
6
7
8
9
10
11
operator(inputs: [a, b, c], outputs: [d, e]) {x, y, z -> }
selector(inputs: [d], outputs: [f, out]) { }
prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }
//Send the poisson indicating the number of operators than need to be terminated before we can continue
final pill = new CountingPoisonPill(3)
a << pill
//Wait for all operators to terminate
pill.join()
//At least 3 operators should be terminated by now
CountingPoisonPill 類別的 termination(終止)屬性是一個常規的 Promise<Boolean>,因此具有許多方便的屬性。
1
2
3
4
5
6
7
8
9
10
11
//Send the poisson indicating the number of operators than need to be terminated before we can continue
final pill = new CountingPoisonPill(3)
pill.termination.whenBound {println "Reporting asynchronously that the network has been stopped"}
a << pill
if (pill.termination.bound) println "Wow, that was quick. We are done already!"
else println "Things are being slow today. The network is still running."
//Wait for all operators to terminate
assert pill.termination.get()
//At least 3 operators should be terminated by now
CountingPoisonPill 的立即變體也可用 - ImmediateCountingPoisonPill(立即計數毒藥丸)。
1
2
3
4
5
6
7
def op1 = selector(inputs: [a, b, c], outputs: [d, e]) {value, index -> }
def op2 = selector(inputs: [d], outputs: [f, out]) { }
def op3 = prioritySelector(inputs: [e, f], outputs: [b]) {value, index -> }
final pill = new ImmediateCountingPoisonPill(3)
a << pill
pill.join()
ImmediateCountingPoisonPill 將安全且立即地關閉資料流網路,即使網路中包含選擇器參與回饋迴路,而普通的非立即毒藥丸將無法做到這一點。
毒藥策略
若要使用 PoisonPill 正確關閉網路,您必須識別要傳送 PoisonPill 的適當通道集。PoisonPill 將以通常的方式透過通道和下游處理器在網路中擴散。通常,傳送 PoisonPill 的正確通道將是那些作為網路資料來源的通道。對於一般情況或複雜網路,這可能難以實現。另一方面,對於訊息流動方向佔主導地位的網路,PoisonPill 提供了一種非常直接的方式來優雅地關閉整個網路。
終止提示與技巧
請注意,GPars tasks(任務)會傳回 DataflowVariable(資料流變數),該變數會在任務完成後立即繫結到值。下面的「終結器」運算子利用了 DataflowVariables 是 DataflowReadChannel 介面的實作這一事實,因此可以被運算子消耗。一旦兩個任務完成,運算子將會向下傳送一個 PoisonPill 到 q 通道,以便在消費者處理完所有資料後立即停止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.group.NonDaemonPGroup
def group = new NonDaemonPGroup()
final DataflowQueue q = new DataflowQueue()
// final destination
def customs = group.operator(inputs: [q], outputs: []) { value ->
println "Customs received $value"
}
// big producer
def green = group.task {
(1..100).each {
q << 'green channel ' + it
sleep 10
}
}
// little producer
def red = group.task {
(1..10).each {
q << 'red channel ' + it
sleep 15
}
}
def terminator = group.operator(inputs: [green, red], outputs: []) { t1, t2 ->
q << PoisonPill.instance
}
customs.join()
group.shutdown()
將毒藥丸保留在特定網路內
如果您的網路將值透過通道傳遞到網路外部的實體,您可能需要在網路邊界停止 PoisonPill 訊息。這可以很容易地透過在每個此類通道上放置一個單輸入單輸出篩選運算子來實現。
1
2
3
operator(networkLeavingChannel, otherNetworkEnteringChannel) {value ->
if (!(value instanceOf PoisonPill)) bindOutput it
}
Pipeline(管道)DSL 在這裡也可能很有用
1
networkLeavingChannel.filter { !(it instanceOf PoisonPill) } into otherNetworkEnteringChannel
查看 Pipeline DSL 部分,以了解有關管道的更多資訊。 |
優雅關閉
GPars 提供了一種關閉資料流網路的通用方法。與前面提到的機制不同,這種方法將保持網路運行,直到所有訊息都得到處理,然後優雅地關閉所有運算子,並讓您知道何時發生這種情況。但是,您必須付出適度的效能損失。這是不可避免的,因為我們需要追蹤網路內部發生的情況。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import groovyx.gpars.dataflow.DataflowBroadcast
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.dataflow.operator.component.GracefulShutdownListener
import groovyx.gpars.dataflow.operator.component.GracefulShutdownMonitor
import groovyx.gpars.group.DefaultPGroup
import groovyx.gpars.group.PGroup
PGroup group = new DefaultPGroup(10)
final a = new DataflowQueue()
final b = new DataflowQueue()
final c = new DataflowQueue()
final d = new DataflowQueue<Object>()
final e = new DataflowBroadcast<Object>()
final f = new DataflowQueue<Object>()
final result = new DataflowQueue<Object>()
final monitor = new GracefulShutdownMonitor(100);
def op1 = group.operator(inputs: [a, b], outputs: [c], listeners: [new GracefulShutdownListener(monitor)]) {x, y ->
sleep 5
bindOutput x + y
}
def op2 = group.operator(inputs: [c], outputs: [d, e], listeners: [new GracefulShutdownListener(monitor)]) {x ->
sleep 10
bindAllOutputs 2*x
}
def op3 = group.operator(inputs: [d], outputs: [f], listeners: [new GracefulShutdownListener(monitor)]) {x ->
sleep 5
bindOutput x + 40
}
def op4 = group.operator(inputs: [e.createReadChannel(), f], outputs: [result], listeners: [new GracefulShutdownListener(monitor)]) {x, y ->
sleep 5
bindOutput x + y
}
100.times{a << 10}
100.times{b << 20}
final shutdownPromise = monitor.shutdownNetwork()
100.times{assert 160 == result.val}
shutdownPromise.get()
[op1, op2, op3, op4]*.join()
group.shutdown()
首先,我們需要一個 GracefulShutdownMonitor(優雅關閉監視器)的實例,它將協調關閉過程。它依賴於附加到所有運算子/選擇器的 GracefulShutdownListener(優雅關閉監聽器)的實例。這些監聽器會觀察各自的處理器及其輸入通道,並向共享的 GracefulShutdownMonitor 報告。一旦在 GracefulShutdownMonitor 上呼叫 shutdownNetwork(),它將定期檢查回報的活動,查詢運算子的狀態以及輸入通道中訊息的數量。
請確保在關閉啟動後,沒有新的訊息進入資料流網路,因為這可能會導致網路永遠無法終止。關閉過程應僅在所有資料生產者停止向受監視網路傳送其他訊息後才啟動。
shutdownNetwork() 方法會傳回一個 Promise,因此您可以對其進行通常的一組技巧 - 使用 get() 方法封鎖等待網路終止,使用 whenBound() 方法註冊回呼,或使其透過 then() 方法觸發一整套活動。
應用框架
資料流運算子
和 `選擇器1 可以成功用於為自然適合流程模型的問題構建高階特定領域框架。
在 GPars Dataflow 之上構建流程框架
GPars 資料流可以被視為底層語言級基礎結構。
運算子、選擇器、通道和事件監聽器在語言層面非常有用,例如可以與actor或平行集合組合使用。每當需要非同步處理透過一個或多個通道傳來的事件時,資料流運算子或小型資料流網路可能非常適合。與任務不同,運算子是輕量級的,並且在沒有訊息要處理時會釋放執行緒。與 actor 不同,運算子會透過通道間接定址,並且可以輕鬆地將來自多個通道的訊息合併為一個動作。
或者,運算子可以被視為連續函數,它們會立即且重複地將其輸入值轉換為輸出。我們認為,對並行友好的通用程式設計語言應提供這種抽象類型。
同時,資料流元素可以輕鬆地用作構建特定領域工作流程類框架的構建模組。這些框架可以提供針對單一問題領域的更高階抽象,這對於通用語言級程式庫來說是不合適的。然後將每個更高階的概念對應到(可能有多個)GPars 概念。
例如,解決資料探勘問題的網路可能由多個資料來源、資料清理節點、分類節點、報告節點和其他節點組成。另一方面,影像處理網路可能需要專門用於影像壓縮和格式轉換的節點。類似地,用於資料加密、mp3 編碼、工作流程管理以及許多其他從基於資料流的解決方案中受益的領域的網路。這些網路在許多方面都會有所不同 - 網路中節點的類型、事件的類型和頻率、負載平衡方案、分支的潛在限制、視覺化、除錯和記錄的需求、使用者定義網路和與其互動的方式等等。
更高階的應用程式特定框架應努力提供最適合給定領域的抽象,並隱藏 GPars 的複雜性。
例如,使用者在螢幕上操作的網路視覺圖通常不應顯示參與網路的所有通道。很少有助於解決方案核心的除錯或記錄通道是首先要考慮排除的良好候選者。此外,協調負載平衡或優雅關閉等方面的通道和生命週期事件監聽器可能不會向使用者公開,儘管它們將是產生和執行網路的一部分。
同樣,特定領域模型中的單一通道實際上將轉換為多個通道,可能會有一個或多個記錄/轉換/篩選運算子將它們連接在一起。與節點關聯的函數很可能會使用一些額外的基礎結構程式碼來包裝,以形成運算子的主體。
GPars 為您提供了底層元件,應用程式特定框架可以完全抽象化最終使用者。這使 GPars 與領域無關且通用,但在實作層面仍然有用。
管線 DSL
用於構建運算子管道的 DSL
可以進一步簡化建構資料流網路。GPars 為構建(主要是線性)運算子管道的常見情境提供了方便的快捷方式。
1
2
3
4
5
6
7
8
9
10
def toUpperCase = {s -> s.toUpperCase()}
final encrypt = new DataflowQueue()
final DataflowReadChannel encrypted = encrypt | toUpperCase | {it.reverse()} | {'###encrypted###' + it + '###'}
encrypt << "I need to keep this message secret!"
encrypt << "GPars can build linear operator pipelines really easily"
println encrypted.val
println encrypted.val
這可以讓您不必直接建立、連線和操作構成管道的所有通道和運算子。pipe 運算子可讓您將一個函數/運算子/處理程序的輸出掛接到另一個函數/運算子/處理程序的輸入。就像在命令列上連結系統處理程序一樣。
pipe 運算子是更通用的 chainWith() 方法的方便簡寫
1
2
3
4
5
6
7
8
9
10
def toUpperCase = {s -> s.toUpperCase()}
final encrypt = new DataflowQueue()
final DataflowReadChannel encrypted = encrypt.chainWith toUpperCase chainWith {it.reverse()} chainWith {'###encrypted###' + it + '###'}
encrypt << "I need to keep this message secret!"
encrypt << "GPars can build linear operator pipelines really easily"
println encrypted.val
println encrypted.val
將管道與直接運算子組合
由於每個運算子管道都有一個入口和一個出口通道,因此可以將管道連線到更複雜的運算子網路中。只有您的想像力可以限制您在同一個網路定義中混合使用管道、通道和運算子的能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def toUpperCase = {s -> s.toUpperCase()}
def save = {text ->
//Just pretending to be saving the text to disk, database or whatever
println 'Saving ' + text
}
final toEncrypt = new DataflowQueue()
final DataflowReadChannel encrypted = toEncrypt.chainWith toUpperCase chainWith {it.reverse()} chainWith {'###encrypted###' + it + '###'}
final DataflowQueue fork1 = new DataflowQueue()
final DataflowQueue fork2 = new DataflowQueue()
splitter(encrypted, [fork1, fork2]) //Split the data flow
fork1.chainWith save //Hook in the save operation
//Hook in a sneaky decryption pipeline
final DataflowReadChannel decrypted = fork2.chainWith {it[15..-4]} chainWith {it.reverse()} chainWith {it.toLowerCase()}
.chainWith {'Groovy leaks! Check out a decrypted secret message: ' + it}
toEncrypt << "I need to keep this message secret!"
toEncrypt << "GPars can build operator pipelines really easy"
println decrypted.val
println decrypted.val
1
2
3
4
5
6
7
8
9
10
11
12
13
14
final SyncDataflowQueue queue = new SyncDataflowQueue()
final result = queue.chainWith {it * 2}.chainWith {it + 1} chainWith {it * 100}
Thread.start {
5.times {
println result.val
}
}
queue << 1
queue << 2
queue << 3
queue << 4
queue << 5
連接管道
可以使用into()方法連接兩個管道(或通道)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
final encrypt = new DataflowQueue()
final DataflowWriteChannel messagesToSave = new DataflowQueue()
encrypt.chainWith toUpperCase chainWith {it.reverse()} into messagesToSave
task {
encrypt << "I need to keep this message secret!"
encrypt << "GPars can build operator pipelines really easy"
}
task {
2.times {
println "Saving " + messagesToSave.val
}
}
加密管道的輸出直接連接到儲存管道的輸入(在我們的案例中是一個單一通道)。
分支資料流
當需要將管道/通道的輸出複製到多個後續管道/通道時,split()方法將會有所幫助
1
2
3
4
5
final encrypt = new DataflowQueue()
final DataflowWriteChannel messagesToSave = new DataflowQueue()
final DataflowWriteChannel messagesToLog = new DataflowQueue()
encrypt.chainWith toUpperCase chainWith {it.reverse()}.split(messagesToSave, messagesToLog)
接入管道
與split()類似,tap()方法允許您將資料流分支到多個通道。然而,在某些情況下,接入會稍微方便一些,因為它將兩個新分支中的一個視為管道的後繼者。
1
queue.chainWith {it * 2}.tap(logChannel).chainWith{it + 1}.tap(logChannel).into(PrintChannel)
合併通道
合併允許您將多個讀取通道連接為單一資料流運算元的輸入。作為第二個參數傳遞的函數需要接受與要合併的通道數量一樣多的參數,每個參數將保存相應通道的值。
1
maleChannel.merge(femaleChannel) {m, f -> m.marry(f)}.into(mortgageCandidatesChannel)
分離
分離是與合併相反的操作。提供的閉包返回一個值列表,每個值都將輸出到具有相應位置索引的輸出通道。
1
queue1.separate([queue2, queue3, queue4]) {a -> [a-1, a, a+1]}
選擇
binaryChoice()和choice()方法允許您根據閉包的返回值將值發送到兩個(或多個)輸出通道中的一個。
1
2
queue1.binaryChoice(queue2, queue3) {a -> a > 0}
queue1.choice([queue2, queue3, queue4]) {a -> a % 3}
過濾
filter()方法允許使用布林謂詞過濾管道中的資料。
1
2
3
4
5
6
7
8
9
10
final DataflowQueue queue1 = new DataflowQueue()
final DataflowQueue queue2 = new DataflowQueue()
final odd = {num -> num % 2 != 0 }
queue1.filter(odd) into queue2
(1..5).each {queue1 << it}
assert 1 == queue2.val
assert 3 == queue2.val
assert 5 == queue2.val
空值
如果鏈式函數返回null值,通常會將其作為有效值沿管道傳遞。若要向運算元指示不應將任何值進一步向下傳遞到管道,則必須返回NullObject.nullObject實例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
final DataflowQueue queue1 = new DataflowQueue()
final DataflowQueue queue2 = new DataflowQueue()
final odd = {num ->
if (num == 5) return null //null values are normally passed on
if (num % 2 != 0) return num
else return NullObject.nullObject //this value gets blocked
}
queue1.chainWith odd into queue2
(1..5).each {queue1 << it}
assert 1 == queue2.val
assert 3 == queue2.val
assert null == queue2.val
自訂執行緒池
所有管道 DSL 方法都允許指定自訂執行緒池或PGroups
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
channel | {it * 2}
channel.chainWith(closure)
channel.chainWith(pool) {it * 2}
channel.chainWith(group) {it * 2}
channel.into(otherChannel)
channel.into(pool, otherChannel)
channel.into(group, otherChannel)
channel.split(otherChannel1, otherChannel2)
channel.split(otherChannels)
channel.split(pool, otherChannel1, otherChannel2)
channel.split(pool, otherChannels)
channel.split(group, otherChannel1, otherChannel2)
channel.split(group, otherChannels)
channel.tap(otherChannel)
channel.tap(pool, otherChannel)
channel.tap(group, otherChannel)
channel.merge(otherChannel)
channel.merge(otherChannels)
channel.merge(pool, otherChannel)
channel.merge(pool, otherChannels)
channel.merge(group, otherChannel)
channel.merge(group, otherChannels)
channel.filter( otherChannel)
channel.filter(pool, otherChannel)
channel.filter(group, otherChannel)
channel.binaryChoice( trueBranch, falseBranch)
channel.binaryChoice(pool, trueBranch, falseBranch)
channel.binaryChoice(group, trueBranch, falseBranch)
channel.choice( branches)
channel.choice(pool, branches)
channel.choice(group, branches)
channel.separate( outputs)
channel.separate(pool, outputs)
channel.separate(group, outputs)
覆寫預設的 PGroup
為了避免為每個管道 DSL 方法單獨指定 PGroup,您可以覆寫預設資料流 PGroup 的值。
1
2
3
4
5
Dataflow.usingGroup(group) {
channel.choice(branches)
}
//Is identical to
channel.choice(group, branches)
Dataflow.usingGroup()方法將給定程式碼區塊的預設資料流 PGroup 值重置為指定的值。
管線建構器
Pipeline類別為運算元管道提供了一個直觀的建構器。與直接鏈接通道相比,使用Pipeline類別的最大好處是能夠輕鬆地將自訂執行緒池/群組應用於沿著建構鏈的所有運算元。可用的方法和重載運算元與直接在通道上可用的方法和運算元相同。
使用Pipeline類別的最大好處是易於使用 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import groovyx.gpars.dataflow.DataflowQueue
import groovyx.gpars.dataflow.operator.Pipeline
import groovyx.gpars.scheduler.DefaultPool
import groovyx.gpars.scheduler.Pool
final DataflowQueue queue = new DataflowQueue()
final DataflowQueue result1 = new DataflowQueue()
final DataflowQueue result2 = new DataflowQueue()
final Pool pool = new DefaultPool(false, 2)
final negate = {-it}
final Pipeline pipeline = new Pipeline(pool, queue)
pipeline | {it * 2} | {it + 1} | negate
pipeline.split(result1, result2)
queue << 1
queue << 2
queue << 3
assert -3 == result1.val
assert -5 == result1.val
assert -7 == result1.val
assert -3 == result2.val
assert -5 == result2.val
assert -7 == result2.val
pool.shutdown()
透過管線 DSL 傳遞建構參數
您很可能經常需要能夠將其他初始化參數傳遞給運算元,例如要附加的監聽器或maxForks的值。就像直接建構運算元一樣,管道 DSL 方法接受一個可選的參數映射以傳遞。
1
new Pipeline(group, queue1).merge([maxForks: 4, listeners: [listener]], queue2) {a, b -> a + b}.into queue3
實作
GPars 中的資料流並行處理基於與Actor支援相同的原則。所有資料流任務共享一個執行緒池,因此通過Dataflow.task()工廠方法建立的執行緒數量不必對應於系統所需的實際執行緒數量。PGroup.task()工廠方法可用於將建立的任務附加到群組。由於每個群組都定義了自己的執行緒池,因此您可以像使用 actors 一樣輕鬆地圍繞不同的執行緒池組織任務。
組合 Actors 和 資料流並行處理
好消息是,您可以根據您認為適合手頭特定問題的任何方式組合 actors 和 資料流並行處理
。您可以自由地使用 Actors 中的資料流變數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final DataflowVariable a = new DataflowVariable()
final Actor doubler = Actors.actor {
react {message->
a << 2 * message
}
}
final Actor fakingDoubler = actor {
react {
doubler.send it //send a number to the doubler
println "Result ${a.val}" //wait for the result to be bound to 'a'
}
}
fakingDoubler << 10
在範例中,您會看到fakingDoubler同時使用訊息和DataflowVariable與doubler Actor 通訊。
使用普通的 Java 線程
DataflowVariable以及DataflowQueue類別顯然可以從您的應用程式的任何執行緒中使用,而不僅僅是從Dataflow.task()建立的任務中使用。請考慮以下範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import groovyx.gpars.dataflow.DataflowVariable
final DataflowVariable a = new DataflowVariable<String>()
final DataflowVariable b = new DataflowVariable<String>()
Thread.start {
println "Received: $a.val"
Thread.sleep 2000
b << 'Thank you'
}
Thread.start {
Thread.sleep 2000
a << 'An important message from the second thread'
println "Reply: $b.val"
}
我們正在建立兩個普通的java.lang.Thread實例,它們使用兩個資料流變數交換資料。顯然,在這種情況下,無論是 Actor 生命週期方法,還是傳送/回應功能或執行緒池都不會生效。
同步變數與通道
當使用非同步資料流通道時,除了讀取器必須等待值可用於取用的事實外,通訊雙方仍然完全獨立。寫入器不會等待其訊息被取用。讀取器會在值出現時立即取得值。另一方面,同步通道可以將寫入器與讀取器以及多個讀取器之間同步。
當您需要提高決定性的程度時,這特別有用。
當使用同步通訊時,非同步通訊施加的寫入器到讀取器的部分排序會得到讀取器到寫入器的部分排序的補充。換句話說,可以保證讀取器在從同步通道讀取值之前所做的任何操作都先於寫入器在寫入該值之後所做的任何操作。此外,使用同步通訊,寫入器永遠不會超前於讀取器太遠,這簡化了對系統的推理,並減少了管理資料生產速度以避免系統超載的需求。
同步資料流佇列
SyncDataflowQueue類別應使用於點對點(1:1 或 n:1)通訊。寫入佇列的每個訊息都將被一個讀取器取用。寫入器會被阻塞,直到其訊息被取用,讀取器會被阻塞,直到有值可供讀取。
同步通道會阻塞寫入器和讀取器,直到所有參與方都準備好 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import groovyx.gpars.dataflow.SyncDataflowQueue
import groovyx.gpars.group.NonDaemonPGroup
/**
* Shows how synchronous dataflow queues can be used to throttle fast producer
* when serving data to a slow consumer. Unlike when using asynchronous channels,
* synchronous channels block both the writer and the readers until all parties
* are ready to exchange messages.
*/
def group = new NonDaemonPGroup()
final SyncDataflowQueue channel = new SyncDataflowQueue()
def producer = group.task {
(1..30).each {
channel << it
println "Just sent $it"
}
channel << -1
}
def consumer = group.task {
while (true) {
sleep 500 //simulating a slow consumer
final Object msg = channel.val
if (msg == -1) return
println "Received $msg"
}
}
consumer.join()
group.shutdown()
同步資料流廣播
SyncDataflowBroadcast類別應用於發佈-訂閱(1:n 或 n:m)通訊。
寫入廣播的每個訊息都將被所有訂閱的讀取器取用。寫入器會被阻塞,直到其訊息被所有讀取器取用,讀取器會被阻塞,直到有值可供讀取,並且所有其他訂閱的讀取器也要求該訊息。使用SyncDataflowBroadcast,您會讓所有讀取器同時處理相同的訊息,並等待彼此,然後再取得下一個訊息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import groovyx.gpars.dataflow.SyncDataflowBroadcast
import groovyx.gpars.group.NonDaemonPGroup
/**
* Shows how synchronous dataflow broadcasts can be used to throttle fast producer
* when serving data to slow consumers. Unlike when using asynchronous channels,
* synchronous channels block both the writer and the readers
* until all parties are ready to exchange messages.
*/
def group = new NonDaemonPGroup()
final SyncDataflowBroadcast channel = new SyncDataflowBroadcast()
def subscription1 = channel.createReadChannel()
def fastConsumer = group.task {
while (true) {
sleep 10 //simulating a fast consumer
final Object msg = subscription1.val
if (msg == -1) return
println "Fast consumer received $msg"
}
}
def subscription2 = channel.createReadChannel()
def slowConsumer = group.task {
while (true) {
sleep 500 //simulating a slow consumer
final Object msg = subscription2.val
if (msg == -1) return
println "Slow consumer received $msg"
}
}
def producer = group.task {
(1..30).each {
println "Sending $it"
channel << it
println "Sent $it"
}
channel << -1
}
[fastConsumer, slowConsumer]*.join()
group.shutdown()
同步資料流變數
與DataflowVariable不同,DataflowVariable是非同步的,並且僅阻塞讀取器,直到將值繫結到變數,而SyncDataflowVariable類別提供了一次性資料交換機制,該機制會阻塞寫入器和所有讀取器,直到達到指定的等待參與方數量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import groovyx.gpars.dataflow.SyncDataflowVariable
import groovyx.gpars.group.NonDaemonPGroup
final NonDaemonPGroup group = new NonDaemonPGroup()
//two readers required to exchange the message
final SyncDataflowVariable value = new SyncDataflowVariable(2)
def writer = group.task {
println "Writer about to write a value"
value << 'Hello'
println "Writer has written the value"
}
def reader = group.task {
println "Reader about to read a value"
println "Reader has read the value: ${value.val}"
}
def slowReader = group.task {
sleep 5000
println "Slow reader about to read a value"
println "Slow reader has read the value: ${value.val}"
}
[reader, slowReader]*.join()
group.shutdown()
看板流程描述
KanbanFlow是一個組合物件,它使用資料流抽象來定義多個並行生產者和消費者運算元之間的相依性。
生產者和消費者之間的每個連結都由KanbanLink定義。
在每個KanbanLink內,生產者和消費者之間的通訊遵循KanbanFlow 模式中描述的KanbanFlow模式。它們使用KanbanTray類型的物件向下游傳送產品,並向生產者發出要求進一步產品的訊號。
下圖說明了一個KanbanLink,其中一個生產者、一個消費者和五個編號為 0 到 4 的托盤。托盤編號 0 已用於將產品從生產者運送到消費者,已被消費者清空,現在被發送回生產者的輸入佇列。托盤 1 和 2 等待攜帶等待取用的產品,而托盤 3 和 4 等待被生產者使用。

KanbanFlow物件將生產者連結到消費者,從而建立KanbanLink物件。在此活動過程中,可以建構第二個連結,其中生產者是曾經在先前建立的連結中充當消費者的同一個物件,因此兩個連結會連接起來以建立鏈。
這是一個僅具有一個連結的KanbanFlow範例,例如一個生產者和一個消費者。生產者始終向下游傳送數字 1,而消費者會列印這個數字。
1
2
3
4
5
6
7
8
9
10
11
12
import static groovyx.gpars.dataflow.ProcessingNode.node
import groovyx.gpars.dataflow.KanbanFlow
def producer = node { down -> down 1 }
def consumer = node { up -> println up.take() }
new KanbanFlow().with {
link producer to consumer
start()
// run for a while
stop()
}
若要將產品放入托盤並將托盤向下游傳送,可以使用 send() 方法、<< 運算元,或將托盤用作方法物件。以下幾行是等效的
1
2
3
node { down -> down.send 1 }
node { down -> down << 1 }
node { down -> down 1 }
當使用 take() 方法從輸入托盤中取出產品時,空的托盤會自動釋放。
您應該只呼叫 take() 一次! |
如果您不希望使用空的托盤向下游傳送產品(通常是當ProcessingNode充當篩選器時的情況),則必須釋放托盤以使其保持作用。否則,系統中的托盤數量會減少。
您可以通過呼叫 release() 方法或使用 ~ 運算元(想想「甩掉它」)來釋放托盤。以下幾行是等效的
1
2
node { down -> down.release() }
node { down -> ~down }
各種連結結構
除了線性鏈之外,KanbanFlow還可以將單一生產者連結到多個消費者(如樹狀結構),或將多個生產者連結到單一消費者(收集器),或將上述任何組合連結在一起,從而形成有向無環圖 (DAG)。
KanbanFlowTest類別提供了許多此類結構的範例,包括單一生產者將工作委派給具有多個消費者的情境
-
一種工作竊取策略,其中所有消費者都從下游選擇其產品,
-
一種主僕策略,其中生產者從可用的消費者中選擇,以及
-
一種廣播策略,其中生產者將所有產品傳送給所有消費者。
預設情況下禁止迴圈,但啟用後,它們可以用作所謂的產生器。生產者甚至可以是自己的消費者,每次迴圈都會增加產品值。產生器本身保持無狀態,因為值僅作為在托盤上運行的產品儲存。此類產生器可用於例如延遲序列,或用作後續流程的心跳
。
產生器迴圈
的方法同樣可以應用於收集器,其中收集器不維護任何內部狀態,而是將集合傳送給自己,每次呼叫都會新增產品。
一般而言,ProcessingNode可以連結到自身,以將狀態匯出到它傳送給自己的托盤/產品。然後,對產品的存取在設計上是執行緒安全的。
組合 KanbanFlows
正如KanbanLink物件可以鏈接在一起以形成KanbanFlow一樣,流程本身也可以再次組合以從現有較小的流程形成新的、更大的流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
def firstFlow = new KanbanFlow()
def producer = node(counter)
def consumer = node(repeater)
firstFlow.link(producer).to(consumer)
def secondFlow = new KanbanFlow()
def producer2 = node(repeater)
def consumer2 = node(reporter)
secondFlow.link(producer2).to(consumer2)
flow = firstFlow + secondFlow
flow.start()
自訂並行處理特性
看板系統中的並行處理量由托盤數量決定(有時稱為 WIP = *進行中的工作)。如果串流中沒有托盤,系統則不會執行任何操作
-
只有一個托盤時,系統會被限制為循序執行。
-
當有更多托盤時,並行處理便開始了。
-
當托盤數量多於可用的處理單元時,系統就會開始浪費資源。
托盤的數量可以用多種方式控制。它們通常在啟動流程時設定。
1
2
3
flow.start(0) // start without trays
flow.start(1) // start with one tray per link in the flow
flow.start() // start with the optimal number of trays
除了托盤之外,KanbanFlow 也可能受到其底層 ThreadPool 的限制。例如,大小為 1 的池將不允許太多的並行處理。
KanbanFlows 使用一個預設池,其大小由可用的核心數量決定。這可以通過設定 pooledGroup 屬性來客製化。
經典範例
使用 Dataflow Tasks
實作的埃拉托斯特尼篩法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.task
/**
* Demonstrates concurrent implementation of the Sieve of Eratosthenes using dataflow tasks
*/
final int requestedPrimeNumberCount = 1000
final DataflowQueue initialChannel = new DataflowQueue()
/**
* Generating candidate numbers
*/
task {
(2..10000).each {
initialChannel << it
}
}
/**
* Chain a new filter for a particular prime number to the end of the Sieve
* @param inChannel The current end channel to consume
* @param prime The prime number to divide future prime candidates with
* @return A new channel ending the whole chain
*/
def filter(inChannel, int prime) {
def outChannel = new DataflowQueue()
task {
while (true) {
def number = inChannel.val
if (number % prime != 0) {
outChannel << number
}
}
}
return outChannel
}
/**
* Consume Sieve output and add additional filters for all found primes
*/
def currentOutput = initialChannel
requestedPrimeNumberCount.times {
int prime = currentOutput.val
println "Found: $prime"
currentOutput = filter(currentOutput, prime)
}
同時使用 Dataflow Tasks
和 Operators
的埃拉托斯特尼篩法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.operator
import static groovyx.gpars.dataflow.Dataflow.task
/**
* Demonstrates concurrent implementation of the Sieve of Eratosthenes using dataflow tasks and operators
*/
final int requestedPrimeNumberCount = 100
final DataflowQueue initialChannel = new DataflowQueue()
/**
* Generating candidate numbers
*/
task {
(2..1000).each {
initialChannel << it
}
}
/**
* Chain a new filter for a particular prime number to the end of the Sieve
* @param inChannel The current end channel to consume
* @param prime The prime number to divide future prime candidates with
* @return A new channel ending the whole chain
*/
def filter(inChannel, int prime) {
def outChannel = new DataflowQueue()
operator([inputs: [inChannel], outputs: [outChannel]]) {
if (it % prime != 0) {
bindOutput it
}
}
return outChannel
}
/**
* Consume Sieve output and add additional filters for all found primes
*/
def currentOutput = initialChannel
requestedPrimeNumberCount.times {
int prime = currentOutput.val
println "Found: $prime"
currentOutput = filter(currentOutput, prime)
}

STM 使用者指南
軟體交易記憶體
軟體交易記憶體
(STM)為開發人員提供了用於存取記憶體中資料的交易語義。這與資料庫的概念相似。
當多個執行緒在記憶體中共享資料時,通過將程式碼區塊標記為交易式(原子性),開發人員將資料一致性的責任委託給 STM 引擎。GPars 利用了 Multiverse STM 引擎。
原子地執行一段程式碼
當使用 STM 時,開發人員會將他們的程式碼組織成交易。交易是一段程式碼,它會被原子性執行 - 要嘛 1) 所有程式碼都執行,要嘛 2) 全部都不執行。
無論交易是正常完成還是突然中止,交易式程式碼所使用的資料都會保持一致性。在交易內部執行時,程式碼會獲得一種與其他同時執行的交易隔離的錯覺,因此一個交易中的資料變更在該交易提交之前,在其他交易中是不可見的。這給了我們資料庫交易的 ACID 特性中的 ACI 部分。對於資料庫來說典型的持久性交易方面,通常不是 Stm 的必要條件。
GPars 允許開發人員通過使用 atomic 閉包來指定交易邊界。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import groovyx.gpars.stm.GParsStm
import org.multiverse.api.references.TxnInteger
import static org.multiverse.api.StmUtils.newTxnInteger
public class Account {
private final TxnInteger amount = newTxnInteger(0);
public void transfer(final int a) {
GParsStm.atomic {
amount.increment(a);
}
}
public int getCurrentAmount() {
GParsStm.atomicWithInt {
amount.get();
}
}
}
有幾種不同類型的 atomic 閉包,每種都用於不同的傳回值類型
-
atomic - 傳回 Object
-
atomicWithInt - 傳回 int
-
atomicWithLong - 傳回 long
-
atomicWithBoolean - 傳回 boolean
-
atomicWithDouble - 傳回 double
-
atomicWithVoid - 無傳回值
預設情況下,Multiverse 使用樂觀鎖定策略,並會自動回滾和重試衝突的交易。
開發人員應該避免在其交易式程式碼中執行不可逆轉的操作(例如,寫入主控台、發送電子郵件、發射飛彈等)。為了提高靈活性,可以通過自訂的 atomic blocks 來客製化預設的 Multiverse 設定。
自訂交易屬性
通常,我們希望為某些交易屬性(例如,唯讀交易、鎖定策略、隔離級別等)指定不同的值。createAtomicBlock 方法將建立一個新的 AtomicBlock,並使用提供的數值進行配置
1
2
3
4
5
6
7
8
import groovyx.gpars.stm.GParsStm
import org.multiverse.api.AtomicBlock
import org.multiverse.api.PropagationLevel
final TxnExecutor block = GParsStm.createTxnExecutor(maxRetries: 3000, familyName: 'Custom', PropagationLevel: PropagationLevel.Requires, interruptible: false)
assert GParsStm.atomicWithBoolean(block) {
true
}
然後可以使用客製化的 AtomicBlock,透過指定的設定來建立交易。
AtomicBlock 實例是執行緒安全的,可以在執行緒和交易之間自由重複使用 |
使用 Transaction 物件
原子閉包使用當前 Transaction 作為參數。交易的 Txn 物件控制代碼可以用於手動控制交易。以下範例說明了這一點,我們使用 retry() 方法來封鎖當前交易,直到計數器達到所需的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import groovyx.gpars.stm.GParsStm
import org.multiverse.api.PropagationLevel
import org.multiverse.api.TxnExecutor
import static org.multiverse.api.StmUtils.newTxnInteger
final TxnExecutor block = GParsStm.createTxnExecutor(maxRetries: 3000, familyName: 'Custom', PropagationLevel: PropagationLevel.Requires, interruptible: false)
def counter = newTxnInteger(0)
final int max = 100
Thread.start {
while (counter.atomicGet() < max) {
counter.atomicIncrementAndGet(1)
sleep 10
}
}
assert max + 1 == GParsStm.atomicWithInt(block) { tx ->
if (counter.get() == max) return counter.get() + 1
tx.retry()
}
資料結構
您可能已經在先前的範例中注意到,我們使用專用的資料結構來保存數值。事實是,普通的 Java 類別不支援交易,因此無法直接使用,因為 Multiverse 將無法在並行交易之間安全地共享它們,提交它們或回滾它們。
普通的 Java 類別不支援交易 |
我們需要使用瞭解交易的資料
-
TxnIntRef
-
TxnLongRef
-
TxnBooleanRef
-
TxnDoubleRef
-
TxnRef
您通常會透過 org.multiverse.api.StmUtils 類別的工廠方法來建立它們。
更多資訊
我們決定不重複 Multiverse 網站上已有的資訊。
不幸的是,隨著 Codehaus 的關閉,該網站不再可用。您可以嘗試從 Multiverse 原始碼 中收集更多資訊。
由於我們不清楚 Multiverse 專案的未來,我們將考慮在未來的 GPars 2.0 中使用不同的 STM 實作。

GAE 使用者指南
Google App Engine 整合
GPars 可以在 Google App Engine (GAE) 上執行。它可以作為 Groovy 和 Java GAE 應用程式的一部分,也可以插入到 Gaelyk 中。
Google App Engine 被稱為 GAE |
小型的 GPars App Engine 整合程式庫 提供了將 GAE 服務連接到 GPars 的所有必要基礎結構。儘管您將在 GAE 執行緒上運行並利用 GAE 定時器服務,但高階抽象概念仍然相同。在一些限制下,您仍然可以使用 GPars actors、dataflow、agents、parallel collections 和其他方便的概念。
有關如何在 GAE 上使用 GPars 的詳細資訊,請參閱 GPars App Engine 程式庫。

遠端處理使用者指南
諸如 Actors、Dataflows 和 Agents 等概念並不局限於單個 VM。它們為並行程式設計提供了一個抽象層,使我們能夠將邏輯與底層同步程式碼分開。這些概念可以很容易地擴展到網路中的多個節點。
以下注意事項描述了 GPars 中的 Remoting。
GPars 的 Remoting 是 2014 Google Summer of Code 專案。 |
簡介
為了遠端使用 Actors、Dataflows 或 Agent,引入了一個新的遠端代理物件,並以 Remote 為前綴。
代理物件通常與其本機對應物件具有相同的介面。這允許我們使用它來代替本機對應物件。在底層,代理物件只是通過網路向原始實例發送訊息。
為了在網路間傳輸訊息,使用了 Netty 程式庫。
為了建立代理物件,使用了實例序列化機制(更多資訊請參閱下面的 remote-serialization)。
使用 Remotes 的一般方法如下(詳細資訊如下)
在主機 A 上
-
建立 Remoting 環境並啟動伺服器來處理傳入的請求。
-
在指定的 name 下發布實例
在主機 B 上
-
建立 Remoting 環境
-
向 hostA:port 請求具有指定 name 的實例。將傳回一個 Promise 物件。
-
從 Promise 中取得代理物件。
在這一點上,將為每個請求建立一個新的連線 |
遠端序列化
以下機制用於建立代理物件
object ←(序列化)→ handle ---- [網路] ---- handle ←(序列化)→ 代理物件
此機制的主要優點之一是,傳送回代理物件的引用會反序列化回原始實例。
由於所有訊息在通過網路傳送之前都會被序列化,因此它們必須實作 Serializable 介面。
這是使用內建 Java 序列化機制和 Netty ObjectDecoder/ObjectEncoder 的結果。另一方面,它讓我們可以靈活地將任何自訂物件作為訊息傳送到 Actor,或使用任何類型的 DataflowVariable(s)。
資料流
為了對 Dataflows 使用 Remoting,必須建立一個環境(RemoteDataflows 類別)。在此環境中,可以發布 Dataflows 並從遠端主機檢索。
1
def remoteDataflows = RemoteDataflows.create()
在所有小節中,我們假設已如上所示建立環境。 |
建立環境後,如果我們希望允許其他主機檢索已發布的 Dataflows,則需要啟動伺服器。我們需要提供一個監聽的位址和埠(例如:localhost:11222 或 10.0.0.123:11333)。
1
remoteDataflows.startServer HOST PORT
為了停止伺服器,我們有一個 stopServer() 方法。請注意,start 和 stop 方法都是非同步的,它們不會被封鎖;伺服器會在背景中啟動/停止。
多次執行這些方法或以錯誤的順序執行它們會引發異常。
為了僅從遠端主機檢索實例,不需要啟動伺服器。 |
DataflowVariable
DataflowVariable 是 Dataflows 子系統的核心部分,它獲得了 Remoting 的功能。其他結構(?)和子系統依賴於它。
在環境中發布變數很簡單,只需
1
2
def variable = new DataflowVariable()
remoteDataflows.publish variable "my-first-variable"
這會在給定的名稱下註冊變數,因此當收到對名稱為 my-first-variable 的變數的請求時,該變數可以被傳送到遠端主機。
請務必記住,以相同的名稱發布另一個變數將會覆蓋先前的變數,並且後續的請求將會傳送新發布的變數。
變數檢索的完成方式是
1
2
def remoteVariablePromise = remoteDataflows.getVariable HOST, PORT, "my-first-variable"
def remoteVariable = remoteVariablePromise.get()
getVariable 方法是非封鎖的,它會傳回一個 Promise 物件,該物件最終將會保存該變數的代理物件。此代理與 DataflowVariable 具有相同的介面,並且可以像一般變數一樣無縫使用。
若要探索完整範例,請參閱我們的:groovyx.gpars.samples.remote.dataflow.variable 程式碼
DataflowBroadcast
可以訂閱遠端主機上的 DataflowBroadcast。若要執行此操作,我們必須先發布它(假設環境已經存在)
1
2
def stream = new DataflowBroadcast()
remoteDataflows.publish stream "my-first-broadcast"
然後在另一個主機上,可以檢索它
1
2
def readChannelPromise = remoteDataflows.getReadChannel HOST, PORT, "my-first-broadcast"
def readChannel = readChannelPromise.get()
代理物件與 ReadChannel 具有相同的介面,並且可以與一般 DataflowBroadcast 的 ReadChannel 以相同的方式使用。
若要探索完整範例,請參閱:groovyx.gpars.samples.remote.dataflow.broadcast
DataflowQueue
DataflowQueue 功能接收到類似的功能,並且像這樣發布
1
2
def queue = new DataflowQueue()
remoteDataflows.publish queue, "my-first-queue"
以類似的方式,我們可以從遠端主機檢索它
1
2
def queuePromise = remoteDataflows.getQueue HOST, PORT, "my-first-queue"
def queue = queuePromise.get()
新的項目可以推送到遠端代理的佇列中。這些元素會通過網路傳送到原始實例,並推送到其中。
檢索命令會向原始實例發送元素請求。
從概念上講,遠端代理是一個介面 - 它只是向原始實例發送請求。
若要探索完整範例,請參閱:groovyx.gpars.samples.remote.dataflow.queue 或 groovyx.gpars.samples.remote.dataflow.queuebalancer
Actors (演員)
Remote Actors
子系統的設計方式類似。
若要啟動 RemoteActors 類別,必須建立一個環境。然後在此環境中,可以發布 Actors 實例或從遠端主機檢索。
1
def remoteActors = RemoteActors.create()
1
2
def actor = ...
remoteActors.publish actor, "actor-name"
1
2
def actorPromise = remoteActors.get HOST, PORT, "actor-name"
def remoteActor = actorPromise.get()
可以加入遠端 Actor,但是這會被封鎖,直到原始 Actor 完成其工作。也支援傳送回覆和 sendAndWait 方法。
可以將任何物件作為訊息傳送到 Actor,但是請記住,它必須是 Serializable。
請參閱範例:groovyx.gpars.samples.remote.actor
遠端 Actor 名稱
RemoteActors 類別環境可以使用名稱來識別。若要建立具有名稱的環境,請使用
1
def remoteActors = RemoteActors.create "test-group-1"
在這個內容中發布的Actors可透過提供特殊的 Actor URL 來存取。
例如:在這個內容中以 actor 名稱發布一個 actor,則可透過 URL "test-group-1/actor" 來存取。
1
def actor = remoteActors.get "test-group-1/actor"
持有此 actor 的實例的主機和端口會自動決定。
呼叫 get 方法會向 255.255.255.255 發送廣播查詢,以搜尋具有該特定名稱的內容中的 actor。符合條件的實例會回應此查詢,並提供必要的主機和端口等資訊。
Agents (代理人)
遠端代理程式
(Remote Agent) 系統的設計方式類似。
首先,必須建立一個 RemoteAgents 類別內容。在這個內容中,可以從遠端主機發布或擷取 Agents。
1
def remoteAgents = RemoteAgents.create()
1
2
def agent = ...
remoteAgents.publish agent, "agent-name"
1
2
def agentPromise = remoteAgents.get HOST, PORT, "agent-name"
def remoteAgent = agentPromise.get()
有兩種方法可以執行用於更新遠端 Agent 實例狀態的閉包 (closure)。
-
remote - 閉包會被序列化並傳送至原始實例,並在該內容中執行。
-
local - 會先擷取目前狀態,然後在發起更新的位置執行閉包,接著將更新的值傳送至原始實例。對 Agent 的並行變更會等待此程序結束。
預設情況下,遠端 Agent 使用 remote 執行原則,但如果需要,我們可以變更它。
1
2
3
def agentPromise = remoteAgents.get HOST, PORT, "agent"
def remoteAgent = agentPromise.get()
remoteAgent.executionPolicy = AgentClosureExecutionPolicy.LOCAL

一般 GPars 提示
分組
高階並行概念,例如 Agents、Actors 或 Dataflow 任務和運算子,可以圍繞共享的執行緒池分組。PGroup 類別及其子類別代表圍繞執行緒池的方便 GPars 包裝器。使用群組的工廠方法建立的物件將共享該群組的執行緒池。
1
2
3
4
5
6
7
8
9
10
11
def group1 = new DefaultPGroup()
def group2 = new NonDaemonPGroup()
group1.with {
task {...}
task {...}
def op = operator(...) {...}
def actor = actor{...}
def anotherActor = group2.actor{...} //will belong to group2
def agent = safe(0)
}
Java API
GPars 的許多功能可以像從 Groovy 一樣從 Java 使用。請查看本使用者指南
的「Java API - 從 Java 使用 GPars
」章節。然後試用基於 Maven 的獨立 Java 示範應用程式。
隨身攜帶 GPars! |
效能
您在 Groovy 中撰寫的程式碼可以像用 Java、Scala 或任何其他程式設計語言撰寫的程式碼一樣快。這應該不足為奇,因為 GPars 在技術上是一個紮實且美味的 Java 製成的蛋糕,上面覆蓋著 Groovy DSL 糖霜。
然而,與 Java 不同的是,使用 GPars 以及其他對 DSL 友好的語言,您很可能會免費體驗到有用的程式碼速度提升。這種速度提升來自於應用程式更好、更簡潔的設計。
使用並行 DSL 進行編碼將為您提供更小的程式碼庫,其中程式碼將並行基本元素用作語言建構。因此,建立強大的並行應用程式、識別潛在的瓶頸或錯誤,然後消除它們會容易得多。
雖然整個 使用者指南
都在描述如何使用 Groovy 和 GPars 建立美觀且強大的並行程式碼,但我們想使用其中一些技巧來強調一些可以透過一些程式碼調整或微小的設計妥協來獲得有趣的效能提升之處。
平行集合
諸如 eachParallel()、collectParallel() 之類的平行集合處理方法會在幕後使用 Parallel Array,這是一種有效率的樹狀資料結構。每次呼叫任何平行集合方法時,都必須從原始集合建立此資料結構。因此,當鏈結平行方法呼叫時,您可能會考慮改用 map/reduce API,或者直接使用 ParallelArray API 以避免 Parallel Array 的建立額外負荷。
1
2
3
4
5
6
import groovyx.gpars.GParsPool;
GParsPool.withPool {
people.findAllParallel{it.isMale()}.collectParallel{it.name}.any{it == 'Joe'}
people.parallel.filter{it.isMale()}.map{it.name}.filter{it == 'Joe'}.size() > 0
people.parallelArray.withFilter({it.isMale()} as Predicate).withMapping({it.name} as Mapper).any{it == 'Joe'} != null
}
在許多情況下,將池大小從預設值變更可以為您帶來效能優勢。特別是如果您的任務執行 IO 操作,例如檔案或資料庫存取、網路等。因此,增加池中的執行緒數量可能會提升效能。
1
2
3
4
import groovyx.gpars.GParsPool;
GParsPool.withPool(50) {
...
}
由於您提供給平行集合處理方法的閉包會頻繁且並行地執行,因此您可以進一步將它們轉換為 Java,從中獲得稍微的收益。
Actors (演員)
GPars actors 速度很快。DynamicDispatchActors 和 ReactiveActors 的速度大約是 DefaultActors 的兩倍,因為它們不必在後續訊息到達之間維持隱含狀態。實際上,DefaultActors 的效能與 Scala 中的 actors 並駕齊驅,而您很少聽到 Scala 的 actors 速度慢。
如果頂級效能是您正在尋找的,那麼請識別程式碼中的模式! |
如果您正在尋找頂級效能,那麼一個好的開始是識別 actor 程式碼中的以下模式
1
2
3
4
5
6
7
8
9
10
actor {
loop {
react {msg ->
switch(msg) {
case String:...
case Integer:...
}
}
}
}
1
2
3
4
messageHandler {
when{String msg -> ...}
when{Integer msg -> ...}
}
呼叫 loop 和 react 方法的成本相當高。
將 DynamicDispatchActor 或 ReactiveActor 定義為類別,而不是使用 messageHandler 和 reactor 工廠方法,也將為您帶來更多速度。
1
2
3
4
5
6
7
8
9
class MyHandler extends DynamicDispatchActor {
public void handleMessage(String msg) {
...
}
public void handleMessage(Integer msg) {
...
}
}
現在,將 MyHandler 類別轉換為 Java,以從 GPars 中擠出最後一點效能。
池調整
GPars 允許您將 actors 圍繞執行緒池分組,讓您自由地以任何喜歡的方式組織 actors。嘗試 actor 池大小和類型總是值得的。
FJPool 通常提供比 DefaultPool 更好的特性,但似乎對池中的執行緒數量更敏感。有時,使用 ResizeablePool 或 ResizeableFJPool 可以透過自動消除不需要的執行緒來幫助提高效能。
1
2
3
4
5
6
def attackerGroup = new DefaultPGroup(new ResizeableFJPool(10))
def defenderGroup = new DefaultPGroup(new DefaultPool(5))
def attacker = attackerGroup.actor {...}
def defender = defenderGroup.messageHandler {...}
...
Agents (代理人)
在處理訊息方面,GPars Agents 甚至比 actors 快一點。關於明智地將 agents 圍繞執行緒池分組,然後調整池大小和類型的建議,也適用於 agents 和 actors。使用 agents,您也可以從提交 Java 編寫的閉包作為訊息中受益。
託管環境
託管環境,例如 Google App Engine,可能會對執行緒施加額外的限制。為了讓 GPars 更好地與這些環境整合,可以自訂預設的執行緒工廠和計時器工廠。
諸如 Google App Engine 之類的託管環境會對執行緒施加限制 |
GPars_Config 類別提供了靜態初始化方法,允許第三方註冊他們自己實作的 PoolFactory 和 TimerFactory 介面。然後可以使用這些介面為 Actors、Dataflow 和 PGroups 建立預設池和計時器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class GParsConfig {
private static volatile PoolFactory poolFactory;
private static volatile TimerFactory timerFactory;
public static void setPoolFactory(final PoolFactory pool)
public static PoolFactory getPoolFactory()
public static Pool retrieveDefaultPool()
public static void setTimerFactory(final TimerFactory timerFactory)
public static TimerFactory getTimerFactory()
public static GeneralTimer retrieveDefaultTimer(final String name, final boolean daemon)
public static void shutdown()
}
自訂工廠應在應用程式啟動後立即註冊,以便 Actors 和 Dataflow 能夠將它們用於其預設群組。
關閉
在受管理的環境中,可以使用 GParsConfig.shutdown() 方法來正確關閉所有非同步執行的計時器,並釋放所有執行緒本機變數的記憶體。
在呼叫此方法後,GPars 程式庫無法再提供宣告的服務。
相容性
在託管環境中執行 GPars 時,可能會發生一些進一步的相容性問題。
最明顯的問題可能是 GAE 中缺少 ForkJoinThreadPool 支援。因此,某些服務可能無法使用 Fork/Join 和 GParsPool 等機制。但是,即使使用受管理的非 Java SE 執行緒池,GParsExecutorsPool、Dataflow、Actors、Agents 和 STM 應該也能正常運作。

結論使用者指南
這真是一段瘋狂的旅程,不是嗎? |
現在,在閱讀完本使用者指南後,您肯定準備好建構快速、強大、可靠且並行的應用程式。
您已經看到您可以從許多概念中選擇,並且每個概念都有其自身的適用範圍。選擇正確的概念來應用是成為成功開發人員的關鍵。
如果您覺得可以使用 GPars 來做到這一點,那麼本使用者指南的任務就已完成。
現在,繼續使用 GPars 並享受樂趣吧! |