协程概念,原理(c++和node.js实现)
協(xié)程
什么是協(xié)程
wikipedia 的定義:協(xié)程是一個(gè)無優(yōu)先級(jí)的子程序調(diào)度組件,允許子程序在特點(diǎn)的地方掛起恢復(fù)。
線程包含于進(jìn)程,協(xié)程包含于線程。只要內(nèi)存足夠,一個(gè)線程中可以有任意多個(gè)協(xié)程,但某一時(shí)刻只能有一個(gè)協(xié)程在運(yùn)行,多個(gè)協(xié)程分享該線程分配到的計(jì)算機(jī)資源。
為什么需要協(xié)程
簡(jiǎn)單引入
就實(shí)際使用理解來講,協(xié)程允許我們寫同步代碼的邏輯,卻做著異步的事,避免了回調(diào)嵌套,使得代碼邏輯清晰。code like this:
co(function*(next){let [err,data]=yield fs.readFile("./test.txt",next);//異步讀文件[err]=yield fs.appendFile("./test2.txt",data,next);//異步寫文件//....})()異步 指令執(zhí)行之后,結(jié)果并不立即顯現(xiàn)的操作稱為異步操作。及其指令執(zhí)行完成并不代表操作完成。
協(xié)程是追求極限性能和優(yōu)美的代碼結(jié)構(gòu)的產(chǎn)物。
一點(diǎn)歷史
起初人們喜歡同步編程,然后發(fā)現(xiàn)有一堆線程因?yàn)镮/O卡在那里,并發(fā)上不去,資源嚴(yán)重浪費(fèi)。
然后出了異步(select,epoll,kqueue,etc),將I/O操作交給內(nèi)核線程,自己注冊(cè)一個(gè)回調(diào)函數(shù)處理最終結(jié)果。
然而項(xiàng)目大了之后代碼結(jié)構(gòu)變得不清晰,下面是個(gè)小例子。
async_func1("hello world",func(){async_func2("what's up?",func(){async_func2("oh ,friend!",func(){ //todo something})})})于是發(fā)明了協(xié)程,寫同步的代碼,享受著異步帶來的性能優(yōu)勢(shì)。
程序運(yùn)行是需要的資源:
- cpu
- 內(nèi)存
- I/O (文件、網(wǎng)絡(luò),磁盤(內(nèi)存訪問不在一個(gè)層級(jí),忽略不計(jì)))
協(xié)程的實(shí)現(xiàn)原理(c++和node.js里面的實(shí)現(xiàn))
libco 一個(gè)C++協(xié)程庫實(shí)現(xiàn)
libco 是騰訊開源的一個(gè)C++協(xié)程庫,作為微信后臺(tái)的基礎(chǔ)庫,經(jīng)受住了實(shí)際的檢驗(yàn)。項(xiàng)目地址:https://github.com/Tencent/libco
個(gè)人源碼閱讀項(xiàng)目:https://github.com/yyrdl/libco-code-study (未完結(jié))
libco源代碼文件一共11個(gè),其中一個(gè)是匯編代碼,其余是C++,閱讀起來相對(duì)較容易。
在C++里面實(shí)現(xiàn)協(xié)程要解決的問題有如下幾個(gè):
- 何時(shí)掛起協(xié)程?何時(shí)喚醒協(xié)程?
- 如何掛起、喚醒協(xié)程,如何保護(hù)協(xié)程運(yùn)行時(shí)的上下文?
- 如何封裝異步操作?
前期知識(shí)準(zhǔn)備
何時(shí)掛起,喚醒協(xié)程?
如開始介紹時(shí)所說,協(xié)程是為了使用異步的優(yōu)勢(shì),異步操作是為了避免IO操作阻塞線程。那么協(xié)程掛起的時(shí)刻應(yīng)該是當(dāng)前協(xié)程發(fā)起異步操作的時(shí)候,而喚醒應(yīng)該在其他協(xié)程退出,并且他的異步操作完成時(shí)。
如何掛起、喚醒協(xié)程,如何保護(hù)協(xié)程運(yùn)行時(shí)的上下文?
協(xié)程發(fā)起異步操作的時(shí)刻是該掛起協(xié)程的時(shí)刻,為了保證喚醒時(shí)能正常運(yùn)行,需要正確保存并恢復(fù)其運(yùn)行時(shí)的上下文。
所以這里的操作步驟為:
- 保存當(dāng)前協(xié)程的上下文(運(yùn)行棧,返回地址,寄存器狀態(tài))
- 設(shè)置將要喚醒的協(xié)程的入口指令地址到IP寄存器
- 恢復(fù)將要喚醒的協(xié)程的上下文
這部分操作相應(yīng)的源代碼:
.globl coctx_swap//定義該部分匯編代碼對(duì)外暴露的函數(shù)名 #if !defined( __APPLE__ ) .type coctx_swap, @function #endif coctx_swap:#if defined(__i386__)leal 4(%esp), %eax //sp R[eax]=R[esp]+4 R[eax]的值應(yīng)該為coctx_swap的第一個(gè)參數(shù)在棧中的地址movl 4(%esp), %esp // R[esp]=Mem[R[esp]+4] 將esp指向 &(curr->ctx) 當(dāng)前routine 上下文的內(nèi)存地址,ctx在堆區(qū),現(xiàn)在esp應(yīng)指向reg[0]leal 32(%esp), %esp //parm a : ®s[7] + sizeof(void*) push 操作是以esp的值為基準(zhǔn),push一個(gè)值,則esp的值減一個(gè)單位(因?yàn)槭前礂^(qū)的操作邏輯,從高位往低位分配地址),但ctx是在堆區(qū),所以應(yīng)將esp指向reg[7],然后從eax到-4(%eax)push//保存寄存器值到棧中,實(shí)際對(duì)應(yīng)coctx_t->regs 數(shù)組在棧中的位置(參見coctx.h 中coctx_t的定義)pushl %eax //esp ->parm apushl %ebppushl %esipushl %edipushl %edxpushl %ecxpushl %ebxpushl -4(%eax) //將函數(shù)返回地址壓棧,即coctx_swap 之后的指令地址,保存返回地址,保存到coctx_t->regs[0]//恢復(fù)運(yùn)行目標(biāo)routine時(shí)的環(huán)境(各個(gè)寄存器的值和棧狀態(tài))movl 4(%eax), %esp //parm b -> ®s[0] //切換esp到目標(biāo) routine ctx在棧中的起始地址,這個(gè)地址正好對(duì)應(yīng)regs[0],pop一次 esp會(huì)加一個(gè)單位的值popl %eax //ret func addr regs[0] 暫存返回地址到 EAX//恢復(fù)當(dāng)時(shí)的寄存器狀態(tài)popl %ebx // regs[1]popl %ecx // regs[2]popl %edx // regs[3]popl %edi // regs[4]popl %esi // regs[5]popl %ebp // regs[6]popl %esp // regs[7]//將返回地址壓棧pushl %eax //set ret func addr//將 eax清零xorl %eax, %eax//返回,這里返回之后就切換到目標(biāo)routine了,C++代碼中調(diào)用coctx_swap的地方之后的代碼將得不到立即執(zhí)行ret#elif這部分代碼只是做了寄存器部分的操作。依賴的結(jié)構(gòu)體定義,見文件coctx.h中:
struct coctx_t { #if defined(__i386__)void *regs[ 8 ];//32位機(jī),依次為:ret,ebx,ecx,edx,edi,esi,ebp,eax #elsevoid *regs[ 14 ];//64位機(jī)的情況 #endifsize_t ss_size;//空間大小char *ss_sp;//ESP};調(diào)用coctx_swap 函數(shù)只在文件co_routine.cpp中的co_swap函數(shù)。
保存運(yùn)行棧的操作見co_swap函數(shù)中調(diào)用coctx_swap之前的部分。具體步驟為取當(dāng)前棧頂?shù)刂?(代碼:char c; esp=&c),若不是共享?xiàng)DP蛣t清理下env,若是則判斷共享?xiàng)^(qū)有沒有被占用,被占用則從堆區(qū)申請(qǐng)內(nèi)存保存,然后再分配共享?xiàng)!?/p>
需要注意的是,libco運(yùn)行時(shí)的棧區(qū)不在是傳統(tǒng)意義上的棧區(qū),其空間實(shí)際來自于堆區(qū)。
如何封裝異步操作?
這部分代碼見:
- co_hook_sys_call.cpp
- co_routine.cpp
- co_epoll.cpp
- co_epoll.h
核心思想是hook系統(tǒng)本來的I/O接口,比如socket()函數(shù),和epoll(kqueue)結(jié)合,采用一個(gè)co_eventloop來統(tǒng)一管理,當(dāng)發(fā)現(xiàn)一個(gè)協(xié)程發(fā)起異步操作時(shí),就將其掛起放入等待隊(duì)列,喚醒其他異步操作已經(jīng)完成的協(xié)程??梢月?lián)系libevent里面的event_loop,區(qū)別在在于一個(gè)是操作棧區(qū)和寄存器恢復(fù)協(xié)程,一個(gè)是調(diào)用綁定的回調(diào)函數(shù)。
node.js里面協(xié)程
node.js 的優(yōu)勢(shì):
- node.js天生異步(下面是libuv)
- javascript的閉包特性完成了上下文的保存工作
需要我們做的:
- 實(shí)現(xiàn)同步編程
附上 文章開始時(shí)的代碼:
const fs=require("fs");const co=require("zco");co(function*(next){let [err,data]=yield fs.readFile("./test.txt",next);//異步讀文件[err]=yield fs.appendFile("./test2.txt",data,next);//異步寫文件//....})()JS 中的Generator
Generator是一個(gè)迭代器生成器,也是node.js中實(shí)現(xiàn)協(xié)程的關(guān)鍵。
let gen=function *() {console.log("ok1");var a=yield 1;console.log("a:"+a);var b=yield 2;console.log("b:"+b); }var iterator=gen(); console.log("ok2");console.log(iterator.next(100)); console.log(iterator.next(101)); console.log(iterator.next(102));輸出:
ok2 ok1 { value: 1, done: false } a:101 { value: 2, done: false } b:102 { value: undefined, done: true }從這里我們可以看到其執(zhí)行順序,以及各個(gè)值的變化。iterator.next() 返回的值即yield 之后的表達(dá)式的返回值,yield之前的變量的值即iterator.next方法傳入的值。通過這個(gè)特性,合理包裝即可實(shí)現(xiàn)coroutine.
以下是zco模塊源碼,項(xiàng)目地址:https://github.com/yyrdl/zco :
/*** Created by yyrdl on 2017/3/14.*/ var slice = Array.prototype.slice;var co = function (gen) {var iterator,callback = null,hasReturn = false;var _end = function (e, v) {callback && callback(e, v); //I shoudn't catch the error throwed by user's callbackif(callback==null&&e){//the error should be throwed if no handler instead of catching silentlythrow e;}}var run=function(arg){try {var v = iterator.next(arg);hasReturn = true;v.done && _end(undefined, v.value);} catch (e) {_end(e);}}var nextSlave = function (arg) {hasReturn = false;run(arg);}var next = function () {var arg = slice.call(arguments);if (!hasReturn) {//support fake async operation,avoid error: "Generator is already running"setTimeout(nextSlave, 0, arg);} else {nextSlave(arg);}}if ("[object GeneratorFunction]" === Object.prototype.toString.call(gen)) {//todo: support other Generator implements iterator = gen(next);} else {throw new TypeError("the arg of co must be generator function")}var future = function (cb) {if ("function" == typeof cb) {callback = cb;}run();}return future; }module.exports = co;總結(jié)
以上是生活随笔為你收集整理的协程概念,原理(c++和node.js实现)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 谈谈对协程的理解
- 下一篇: C/C++ 笔试、面试题目大汇总