對協程的一些理解
協程
協程(coroutine)最早由Melvin Conway在1963年提出並實現,一句話定義:協程是用戶態的輕量級的線程
線程和協程
線程和協程經常被放在一起比較;線程一旦被創建出來,編寫者是無法決定什麽時候獲得或者放出時間片的,是由操作系統進行統一調度的;而協程對編寫者來說是可以控制切換的時機,並且切換代價比線程小,因為不需要進行內核態的切換。
協程避免了無意義的調度,由此可以提高性能,但也因此,程序員必須自己承擔調度的責任,同時,協程也失去了標準線程使用多CPU的能力, 但是可用通過多個(進程+多協程)模式來充分利用多CPU。
協程另外一個重要的特點就是:協程是作用在用戶態,操作系統內核對於協程是毫無感知的,這樣一來,協程的創建就類似於普通對象的創建,非常輕量級,從而使你可以在一個線程裏面輕松創建數十萬個協程,就像數十萬次函數調用一樣。可以想象一下,如果是在一個進程裏面創建數十萬個線程,結果該是怎樣可怕。
進程、線程、協程
進程:擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程由操作系統調度。
線程:擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程亦由操作系統調度(標準線程是的)。
協程:和線程一樣共享堆,不共享棧,協程由程序員在協程的代碼裏顯示調度。
對協程的支持
Lua, Go,C#等語言原生支持協程
Java依賴第三方庫,例如最為著名的協程開源庫Kilim
C標準庫裏的函數setjmp和longjmp可以用來實現一種協程。
下面以lua語言為例子了解一下協程的工作機制
function foo(a)
print("foo", a)
return coroutine.yield(2 * a)
end
co = coroutine.create(function ( a, b )
print("co-body_01", a, b)
local r = foo(a + 1)
print("co-body_02", r)
return "end"
end)
print("---main---", coroutine.resume(co, 1, 10))
print("---main---", coroutine.resume(co, "r7"))
運行結果:
D:\>luac text.lua
D:\>lua luac.out
co-body_01 1 10
foo 2
---main--- true 4
co-body_02 r7
---main--- true end
主要利用resume和yield兩個函數進行控制切換的時機,具體描述看如下圖(來源網上):
協程經常被用在遇到io阻塞操作的時候,直接yield讓出cpu,讓下面的程序可以繼續執行,等到操作完成了再重新resume恢復到上一次yield的地方;有沒有覺得這種模式和我們碰到過的異步回調模式有點類似,下面可以進行一個對比。
協程和callback
協程經常被用來和callback進行比較,因為都實現了異步通信的功能;下面以一個具體的場景來描述2者的區別:
A首先要走到B的面前,到B的面前之後,A對B說“Hello”,A說完之後B對A說“Hello”,註意這裏的每個動作之前都有一段的延遲時間
這個場景如果用callback的方式來描述的話,會是這樣:
A.walkto(function ( )
A.say(function ( )
B.say("Hello")
end,"Hello")
end, B)
這邊只用到2層嵌套,如果再多幾層的話,真是非人類代碼了,如果用協程來實現:
co = coroutine.create(function ( )
local current = coroutine.running
A.walto(function ( )
coroutine.resume(current)
end, B)
coroutine.yield()
A.say(function ( )
coroutine.resume(current)
end, "hello")
coroutine.yield()
B.say("hello")
end)
coroutine.resume(co)
結構清晰了不少,協程讓編程者以同步的方式寫成了異步大代碼;
來源網上的一句總結:讓原來要使用異步+回調方式寫的非人類代碼,可以用看似同步的方式寫出來
不管是協程還是callback,本質上其實提供的是一種異步無阻塞的編程模式,下面看看java在這種模式下的嘗試:
java異步無阻塞的編程模式
java語言本身沒有提供協程的支持,但是一些第三方庫提供了支持,比如JVM上早期有kilim以及現在比較成熟的Quasar。但是這裏沒打算就kilim和quasar框架進行介紹;這裏要介紹的是java5中的Future類和java8中的CompletableFuture類。
1.Future使用
ExecutorService es = Executors.newFixedThreadPool(10);
Future<Integer> f = es.submit(() ->{
// 長時間的異步計算
// ……
// 然後返回結果
return 100;
});
//while(!f.isDone())
f.get();
雖然Future以及相關使用方法提供了異步執行任務的能力,但是對於結果的獲取卻是很不方便,只能通過阻塞或者輪詢的方式得到任務的結果。
這種模式暫且叫它偽異步。其實我們想要的是類似Netty中這種模式:
ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port));
future.addListener(new ChannelFutureListener()
{
@Override
public void operationComplete(ChannelFuture future) throws Exception
{
if (future.isSuccess()) {
// SUCCESS
}
else {
// FAILURE
}
}
});
操作完成時自動調用回調方法,終於在java8中推出了CompletableFuture類
2.CompletableFuture使用
CompletableFuture提供了非常強大的Future的擴展功能,可以幫助我們簡化異步編程的復雜性,提供了函數式編程的能力,可以通過回調的方式處理計算結果,並且提供了轉換和組合CompletableFuture的方法。這裏不想介紹更多CompletableFuture的東西,想了解更多CompletableFuture介紹,看一個比較常見的使用場景:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(耗時函數);
Future<Integer> f = future.whenComplete((v, e) -> {
System.out.println(v);
System.out.println(e);
});
System.out.println("other...");
CompletableFuture真正的實現了異步的編程模式
總結
為什麽協程在Java裏一直那麽小眾,Java裏基本上所有的庫都是同步阻塞的,很少見到異步無阻塞的。而且得益於J2EE,以及Java上的三大框架(SSH)洗腦,大部分Java程序員都已經習慣了基於線程,線性的完成一個業務邏輯,很難讓他們接受一種將邏輯割裂的異步編程模型。
對協程的一些理解