動態

詳情 返回 返回

漫談協程(coroutine) - 動態 詳情

一 什麼是協程

協程現在已經不是一個新的技術了,但是由於之前一直在用較低版本的c++,沒什麼機會使用協程。最近寫了不少go的代碼,接觸到了協程,所以想從零開始學習一下協程。

1. 到底什麼是協程

之前聽説協程的時候,大家都講協程就是執行在用户態的微線程,加上go中協程的使用和線程差不多,我也就一直這樣理解了。但是真正定義協程的功能是:可以隨時的掛起和恢復,它允許多個入口點在不同的執行點掛起和恢復,而不是像線程那樣在任何時候都可能被系統強制切換。那麼可以隨時掛起和恢復到底能解決什麼問題呢?下面我們來談談協程的優勢。

2. 協程的優勢

協程擁有輕量,高效,簡單等優勢。

  1. 輕量:協程一般都是在各個語言的層面上做實現,線程仍然是操作系統運算調度的最小單位,比起線程來,創建協程更加輕量。協程有多種實現方式,當我們在一個線程上分配多個協程時,協程之間就不需要考慮鎖機制。
  2. 高效:當我們的線程在執行IO密集型操作時,往往需要等待IO結果,此時操作系統要麼做線程的切換,而頻繁的切換線程是一個和高額的操作,當使用協程的時候,我們在線程內使用協程將操作掛起,等待IO完成時再繼續執行,這樣不會發生線程切換等操作。
  3. 簡化異步編程:在我們使用rpc框架時,框架往往會提供同步,異步等調用方式,當同步調用其他接口時,當前線程會被阻塞,當異步調用其他接口時,就需要你提供一個回調函數,當有結果返回時,由框架將結果回吐給你。這種編程方式是不方便的,協程可以簡化這個操作,後面我們會舉例説明。

下面我會介紹協程是如何產生上述優勢的,行文邏輯如下,在第二個章節,我會介紹,當已知了協程的功能,使用協程的時候,我們如何簡化了異步編程;第三個章節我們會介紹協程是如何實現我們希望的那些功能的。

二 使用協程異步編程

使用異步做網絡編程(實現業務邏輯)時,我們的業務代碼是有嚴格的執行順序的,但是異步的返回是無序的,就使得我們,代碼往往需要一些狀態碼來判斷前置調用是否已經完成,如果再疊加了異常處理這些邏輯的話,代碼邏輯會非常晦澀難懂,而且容易經常性的形成回調地獄。舉個例子,如果我們使用異步回調的方式對一個整型數字做加3的操作,我們有一個加1的函數,加3時需要調用三次:

void AsyncAddOne(int val, std::function<void (int)> callback) {
    std::thread t([value, callback = std::move(callback)] {
        callback(val + 1);
    });
    t.detach();
}

AsyncAddOne(1, [] (int result) {
        AsyncAddOne(result, [] (int result) {
            AsyncAddOne(result, [] (int result) {
                cout << "result is: " << result << endl;
            });
        });
    });

看起來十分的晦澀難懂,現在大部門的服務框架其實已經做了一些優化,比如使用Promise/Future特性。下面只是簡單示意一下:

AddOne.then({return AddOne.then({return AddOnde})})

我們拿一個在日常生產過程中的一段實例來示範Promise/Future特性,
示例如下:這段代碼的邏輯是使用了兩個異步線程分別調用了redis和mysql,拿到結果後做自身的業務處理請求

// 第一個串行任務,CommonTask
trpc::Future<Result> CommonHandler() {
  // 1. do something in common handler
  return MakeReadyFuture<Result1>(res);
}

void HttpHandler() {
  // 1. 處理公共邏輯
  auto http_task = CommonHandler();

  // 2. 任務1完成後,創建並執行並行任務
  auto data_task = http_task.Then([](Future<Result1>&& result1) {
    // 2.1 創建redis任務,通過redis_proxy發起調用, 並返回相關結果,cmd為請求redis的命令
    trpc::Future<Result2> fut_redis_task = redis_proxy->AsyncRedis(cmd);

    // 2.2 創建mysql任務, 通過mysql_proxy發起調用, 並返回相關結果,cmd為請求mysql的命令
    trpc::Future<Result2> fut_mysql_task = mysql_proxy->AsyncMysql(cmd);

    // 將單個任務加入parallel_futs
    parallel_futs.push_back(fut_redis_task);
    parallel_futs.push_back(fut_mysql_task);
    // 若並行任務2.1和2.2都完成了則結束該回調,並進入下一個回調
    auto fut = WhenAll(parallel_futs).Then([](std::vector<Future<Result2>>&& result2) {
      // 分別獲得redis和mysql的result, 進而完成相關任務
      // result[0].GetValue();
      // result[1].GetValue();
      // 3. do something calc handler...

      return trpc::MakeReadyFuture<Resul3t>(res);
    });
    return fut;
  });

  // 回包
  data_task.Then([](Future<Result3>&& result3){
    if (result3.IsReady()) {
      // 4. do something and response to client
      // full succ in reply
    } else {
      // full exception in reply
    }
    SendUnaryResponse(reply);
    // 鏈式調用最後的then可以返回void
  });
}

雖然Future這種模式已經簡化了之前自己寫代碼判斷各個異步任務的完成狀態(實際上是封裝在了Future自身的邏輯中),但是也有一定的編程複雜度,尤其在涉及到錯誤處理的時候。
使用協程可以讓我們像使用一個線程做同步調用一樣,來寫我們的一部調用代碼。具體是如何做到的,可以參照下文的實現。

三 協程是如何實現的

協程的實現方式有很多種,具體到線程這個點上,有M:N和1:N的實現方式,M:N就是在M個線程上啓用N個協程,1:N就是在1個線程上開啓N個協程,這兩種實現區別也是顯而易見的,M:N可以充分利用cpu性能,1:N實現不需要考慮協程間的競爭問題。

我們回顧一下協程需要實現的功能:

  1. 任務掛起
  2. 任務恢復

所以在實現協程時,掛起(co_yield)需要保存當前函數執行的上下文,在恢復執行(co_resume)時需要恢復函數棧幀重新執行。做此類實現一般都需要藉助彙編,這裏列舉幾個協程庫:https://github.com/Tencent/libco
https://github.com/boostorg/fiber
微信的libco同時也hook了recv等系統調用,在執行網絡IO時會自行讓渡,在使用時需要加上特殊的鏈接參數。

後面會對libco做一些分析(未完待續)

Add a new 評論

Some HTML is okay.