1. 程式人生 > >對協程的一些理解

對協程的一些理解

quasar 習慣 -h 獨立 mil 相關 ots clas 性能

協程
協程(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程序員都已經習慣了基於線程,線性的完成一個業務邏輯,很難讓他們接受一種將邏輯割裂的異步編程模型。


對協程的一些理解