在java中使用協程

 

1. 背景知識

現在的操作系統都是支持多任務的,多任務可通過多進程或多線程的方式去實現,進程和線程的對比就不在這裏說了,在多任務的調度上操作系統採取搶佔式和協作式兩種方式,搶佔式是指操作系統給每個任務一定的執行時間片,在到達這個時間片後如任務仍然未釋放對CPU的佔用,那麼操作系統將強制釋放,這是目前多數操作系統採取的方式;協作式是指操作系統按照任務的順序來分配CPU,每個任務執行過程中除非其主動釋放,否則將一直佔據CPU,這種方式非常值得注意的是一旦有任務佔據CPU不放,會導致其他任務餓死的現象,因此操作系統確實不太適合採用這種方式。

說完操作系統多任務的調度方式後,來看看通常程序是如何實現支持高併發的,一種就是典型的基於操作系統提供的多進程或多線程機制,每個任務佔據一個進程或一個線程,當任務中有IO等待等動作時,則將進程或線程放入待調度隊列中,這種方式是目前大多數程序採取的方式,這種方式的壞處在於如想支持高的併發量,就不得不創建很多的進程或線程,而進程和線程都是要消耗不少系統資源的,另外一方面,進程或線程創建太多後,操作系統需要花費很多的時間在進程或線程的切換上,切換動作需要做狀態保持和恢復,這也會消耗掉很多的系統資源;另外一種方式則是每個任務不完全佔據一個進程或線程,當任務執行過程中需要進行IO等待等動作時,任務則將其所佔據的進程或線程釋放,以便其他任務使用這個進程或線程,這種方式的好處在於可以減少所需要的原生的進程或線程數,並且由於操作系統不需要做進程或線程的切換,而是自行來實現任務的切換,其成本會較操作系統切換低,這種方式也就是本文的重點,Coroutine方式,又稱協程方式,這種方式在目前的大多數語言中都有支持。

各種語言在實現Coroutine方式的支持時,多數都採用了Actor Model來實現,Actor Model簡單來說就是每個任務就是一個ActorActor之間通過消息傳遞的方式來進行交互,而不採用共享的方式,Actor可以看做是一個輕量級的進程或線程,通常在一臺4G內存的機器上,創建幾十萬個Actor是毫無問題的,Actor支持Continuations,即對於如下代碼:

Actor

act方法

進行一些處理

創建並執行另外一個Actor

通過消息box阻塞獲取另一個Actor執行的結果

繼續基於這個結果進行一些處理

在支持Continuations的情況下,可以做到消息box阻塞時並不是進程或線程級的阻塞,而只是Actor本身的阻塞,並且在阻塞時可將所佔據的進程或線程釋放給其他Actor使用,Actor Model實現最典型的就是erLang了。

對於Java應用而言,傳統方式下爲了支持高併發,由於一個線程只能用於處理一個請求,即使是線程中其實有很多IO中斷、鎖等待也同樣如此,因此通常的做法是通過啓動很多的線程來支撐高併發,但當線程過多時,就造成了CPU需要消耗不少的時間在線程的切換上,從而出現瓶頸,按照上面對Coroutine的描述,Coroutine的方式理論上而言能夠大幅度的提升Java應用所能支撐的併發量。

2. Java中使用Coroutine

Java尚不能從語言層次上支持Coroutine,也許Java 7能夠支持,目前已經有了一個測試性質的版本<!--[if !supportFootnotes]-->[1]<!--[endif]-->,在Sun JDK 7尚未正式發佈的情況下如希望在Java中使用CoroutineScalaKilim是可以做的選擇,來分別看下。

Scala是現在很火的語言之一,Twitter消息中間件基於Scala編寫更是讓Scala名聲鵲起,除了在語法方面所做出的改進外,其中一個最突出的特色就是Scala ActorScala ActorScala用於實現Coroutine的方式,先來具體看看ScalaCoroutine支持實現的關鍵概念。

Actor

Scala Actor可以看做是一個輕量級的Java Thread,其使用方式和Java Thread基本也一致,繼承Actor,實現act方法,啓動時也是調用start方法,但和Java Thread不同的是,Scala Actor可等待外部發送過來的消息,並進行相應的處理。

Actor的消息發送機制

發送消息到Actor的方式有異步、Future兩種方式,異步即指發送後立即返回,繼續後續流程,使用異步發送的方法爲:actor ! MessageObject,其中消息對象可以爲任何類型,並且Scala還支持一種稱爲case Object的對象,便於在收到消息時做pattern matching

Future方式是指阻塞線程等待消息處理的結果,使用Future方式發送的方法爲:actor !! MessageObject,在等待結果方面,Scala支持不限時等待,限時等待以及等待多個Future或個別Future完成,使用方法如下:

val ft=actor !! MessageObject // Future方式發送消息

val result=ft() // 不限時等待

val results=awaitAll(500,ft1,ft2,ft3)  // 限時等待多個Future返回值

val results=awaitEither(ft1,ft2) // 等待個別future完成

接收消息方通過reply方法返回Future方式所等待的結果。

Actor的消息接收機制

當代碼處於Actoract方法或Actor環境(例如爲Actoract方法調用過來的代碼)中時,可通過以下兩種方式來接收外部發送給Actor的消息:一爲receive方式,二爲react方式,代碼例子如下:

receive{

case MessageObject(args) => doHandle(args)

}

react{

case MessageObject(args) => doHandle(args)

}

receivereact的差別在於receive需要阻塞當前Java線程,react則僅爲阻塞當前Actor,但並不會阻塞Java線程,因此react模式更適合於充分發揮coroutine帶來的原生線程數減少的好處,但react模式有個缺點是react不支持返回。

receivereact都有限時接收的方式,方法爲:receiveWithin(timeout)reactWithin(timeout),超時的消息通過case TIMEOUT的方式來接收。

下面來看基於Scala Actor實現併發處理請求的一個簡單例子。

class Processor extends Actor{

def act(){

loop{

react{

case command:String => doHandle(command)

}

}

}

def doHandle(command:String){

// 業務邏輯處理

}

}

當需要併發執行此Processor時,在處理時需要的僅爲調用以下代碼:

val processor=new Processor()

processor.start

processor ! “Hello”

從以上說明來看,要在舊的應用中使用Scala還是會有一些成本,部署運行則非常簡單,在Scala IDE Plugin編寫了上面的scala代碼後,即生成了java class文件,可直接在jvm中運行。

Kilim是由劍橋的兩位博士開發的一個用於在Java中使用Coroutine的框架,Kilim基於Java語法,先來看看Kilim中的關鍵概念。

Task

可以認爲Task就是Actor,使用方式和Java Thread基本相同,只是繼承的爲Task,覆蓋的爲execute方法,啓動也是調用taskstart方法。

Task的消息發送機制

Kilim中通過Mailbox對象來發送消息,Mailbox的基本原則爲可以有多個消息發送者,但只能有一個消息接收者,發送的方式有同步發送、異步發送和阻塞線程方式的同步發送三種,同步發送是指保證一定能將消息放入發送隊列中,如當前發送隊列已滿,則等待到可用爲止,阻塞的爲當前Task;異步發送則是嘗試將消息放入發送隊列一次,如失敗,則返回false,成功則返回true,不會阻塞Task;阻塞線程方式的同步發送是指阻塞當前線程,並保證將消息發送給接收者,三種方式的使用方法如下:

mailbox.put(messageObject); // 同步發送

mailbox.putnb(messageObject); // 異步發送

mailbox.putb(messageObject); // 阻塞線程方式發送

 

Task的消息接收機制

Kilim中通過Mailbox來接收消息,接收消息的方式有同步接收、異步接收以及阻塞線程方式的同步接收三種,同步接收是指阻塞當前Task,直到接收到消息才返回;異步接收是指立刻返回Mailbox中的消息,有就返回,沒有則返回null;阻塞線程方式的同步接收是指阻塞當前線程,直到接收到消息才返回,使用方法如下:

mailbox.get(); // 同步接收,傳入long參數表示等待的超時時間,單位爲毫秒

mailbox.getnb(); // 異步接收,立刻返回

mailbox.getb(); // 阻塞線程方式接收

下面來看基於Kilim實現併發處理請求的一個簡單例子。

public class Processor extends Task{

private String command;

public Processor(String command){

this.command=command;

}

public void execute() throws Pausable,Exception{

// 業務邏輯處理

}

}

在處理時,僅需調用以下代碼:

Task processor=new Processor(command);

processor.start();

從以上代碼來看,Kilim對於Java人員而言學習門檻更低,但對於需要採用coroutine方式執行的代碼在編譯完畢後,還需要採用Kilimkilim.tools.Weaver類來對這些已編譯出來的class文件做織入,運行時需要用織入後生成的class文件纔行,織入的方法爲:java kilim.tools.Weaver –d [織入後生成的class文件存放的目錄] [需要織入的類文件所在的目錄],目前尚沒有Kilim IDE Plugin可用,因此weaver這個過程還是比較的麻煩。

上面對ScalaKilim做了一個簡單的介紹,在實際Java應用中使用Coroutine時,通常會出現以下幾種典型的更復雜的使用場景,由於Actor模式本身就是異步的,因此其天然對異步場景支持的就非常好,更多的問題會出現在以下幾個同步場景上,分別來看看基於ScalaKilim如何來實現。

Actor同步調用

Actor同步調用是經常會出現的使用場景,主要爲Actor發送消息給其他的Actor處理,並等待結果才能繼續。

n  Scala

對於這種情況,在Scala 2.7.7中,目前可採取的爲以下兩種方法:

一種爲通過Future方式發送消息來實現:

class Processor(command:String) extends Actor{

def act(){

val actor=new NetSenderActor()

val ft=actor !! command

println(ft())

}

}

class NetSenderActor extends Actor{

def act(){

case command:String => {

reply(“received command:”+command)

}

}

}

第二種爲通過receive的方式來實現:

class Processor(command:String) extends Actor{

def act(){

val actor=new NetSenderActor()

actor ! command

var senderResult=””

receive{

case result:String => {

senderResult=result

}

}

println(senderResult)

}

}

class NetSenderActor extends Actor{

def act(){

case command:String => {

sender ! (“received command:”+command)

}

}

}

但這兩種方式其實都不好,因爲這兩種方式都會造成當前Actor的線程阻塞,這也是因爲目前Scala版本對continuations尚不支持的原因,Scala 2.8版本將提供continuations的支持,希望到時能有不需要阻塞Actor線程實現上述需求的方法。

還有一種常見的場景是Actor調一段普通的Scala類,然後那個類中進行了一些處理,並調用了其他Actor,此時在該類中如需要等待Actor的返回結果,也可使用上面兩種方法。

n  Kilim

Kilim中要實現Task之間的同步調用非常簡單,代碼如下:

public class TaskA extends Task{

public void execute() throws Pausable,Exception{

Mailbox<Object> result=new Mailbox<Object>();

Task task=new TaskB(result);

task.start();

Object resultObject=result.get();

System.out.println(resultObject);

}

}

public class TaskB extends Task{

private Mailbox<Object> result;

public TaskB(Mailbox<Object> result){

this.result=result;

}

public void execute() throws Pausable,Exception{

result.put(“result from TaskB”);

}

}

KilimMailbox.get並不會阻塞線程,因此這種方式是完全滿足需求的。

普通Java代碼同步調用Actor

由於已有的應用是普通的Java代碼,經常會出現這樣的場景,就是希望實現在這些Java代碼中同步的調用Actor,並等待Actor的返回結果,但由於ScalaKilim都強調首先必須是在ActorTask的環境下才行,因此此場景更佳的方式應爲Scala Actor(Kilim Task) à Java Code à Scala Actor(Kilim Task),這種場景在對已有的應用中會是最常出現的,來看看在ScalaKilim中如何應對這樣的需求。

n  Scala

目前Scala中如希望在Java Code中調用Scala Actor,並等待其返回結果,暫時還沒辦法,做法只能改爲從Java Code中去調一個ScalaObject,然後在這個Object中調用Actor,並藉助上面提到的receivefuture的方法來獲取返回值,最後將這個返回值返回Java Code

n  Kilim

目前Kilim中如希望實現上面的需求,其實非常簡單,只需要在Java Code的方法上加上Throw Pausable,然後通過mailbox.get來等待Kilim Task返回的結果即可,在Kilim中只要調用棧上的每個方法都有Throw Pausable,就可在這些方法上做等待返回這類的同步操作。

從上面這兩個最常見的需求來看,無疑Kilim更符合需求,但要注意的是對於Kilim而言,如果出現Task call  nonpausable method call pausable method這樣的狀況時,pausable method中如果想執行阻塞當前Task的操作,是無法做到的,只能改造成Task (mailbox上做等待,並傳遞mailbox給後續步驟)  call nonpausable method (傳遞mailbox) call  pausable method (將邏輯轉爲放入一個Task中,並將返回值放入傳遞過來的mailbox),這種狀況在面對spring aop、反射調用等現象時就會出現了,目前kilim 0.6的版本尚未提供更透明的使用方法,不過據kilim作者提供的一個試用版本,其中已經有了對於反射調用的透明化的支持,暫時在目前只能採用上述方法,遷移成本相對較大,也許以後的kilim版本會考慮這樣的場景,提供相應的方法來降低遷移的成本。

3.  性能、所能支撐的併發量對比

在對ScalaKilim有了這些瞭解後,來具體看看採用ScalaKilim後與傳統Java方式在性能、所能支撐的併發量上的對比。

測試模型

採用一個比較簡單的模型進行測試,具體爲有4個線程,這4個線程分別接收到了一定數量的請求,每個請求需要交給另外一個線程去執行,這個線程所做的動作爲循環10次獲取另外一個線程的執行結果,此執行線程所做的動作爲循環1000次拼接一個字符串,然後返回。

實現代碼

由於目前Scala版本對Continuation支持不夠好,但上面的場景中又有此類需求,所以導致Scala版本的代碼寫的比較麻煩一些。

實現代碼以及可運行的環境請從此處下載:http://www.bluedavy.com/open/benchmark.zip到此爲止,基本上對Java中使用Coroutine的相關知識做了一個介紹,總結而言,採用Coroutine方式可以很好的繞開需要啓動太多線程來支撐高併發出現的瓶頸,提高Java應用所能支撐的併發量,但在開發模式上也會帶來變化,並且需要特別注意不能造成線程被阻塞的現象,從開發易用和透明遷移現有Java應用兩個角度而言目前Coroutine方式還有很多不足,但相信隨着越來越多的人在Java中使用Coroutine,其易用性必然是能夠得到提升的。

 

 

 


 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章