協程(coroutine)簡介 - 什麼是協程?
如果涉及到具體的作業系統和CPU等,則為 Linux/amd64
程式記憶體佈局
首先介紹一下記憶體佈局,從高地址到低地址,依次是:
code segment
當然,這個還不完全,還有很多段沒有列出來例如stack和heap中間可能有mmap的記憶體段。此外此處還沒有考慮到多執行緒的情況下,不過為了熱熱身,已經夠了。
棧和幀
棧的作用主要是用來儲存函式呼叫之後的該返回哪裡。通常,呼叫一個函式,進行的步驟有把引數從右向左依次壓棧, 然後把返回地址壓棧,然後開始執行函式。當然,這是實現細節,只是目前比較常用的一種方式。當函式執行完之後,就取出返回地址, 繼續執行。有一個暫存器,PC(Program Counter)就是用來儲存下一條要執行的指令的。
計算機中有一句名言,所有的問題都可以引入一箇中間層來解決,那麼,為什麼會有幀呢?解決了什麼問題?我個人的看法是,將多個 函式呼叫分別包裝起來,從而進行了隔離,當然,這裡的“包裝”是一個抽象概念,實際上,只要你想,仍然可以訪問到其他函式的棧 內的內容。
協程(coroutine)
無棧協程(stackless coroutine)
Python中的協程一般是通過yield來實現,有yield的函式,其返回結果就是一個generator。而Python中的協程就屬於無棧協程。 那我們來看一下generator object長啥樣:
/* _PyGenObject_HEAD defines the initial segment of generator and coroutine objects. */ #define _PyGenObject_HEAD(prefix)\ PyObject_HEAD\ /* Note: gi_frame can be NULL if the generator is "finished" */\ struct _frame *prefix##_frame;\ /* True if generator is being executed. */\ char prefix##_running;\ /* The code object backing the generator */\ PyObject *prefix##_code;\ /* List of weak reference. */\ PyObject *prefix##_weakreflist;\ /* Name of the generator. */\ PyObject *prefix##_name;\ /* Qualified name of the generator. */\ PyObject *prefix##_qualname; typedef struct { /* The gi_ prefix is intended to remind of generator-iterator. */ _PyGenObject_HEAD(gi) } PyGenObject;
為什麼叫做無棧協程呢?因為generator沒有自己的棧,只有它自己的幀。每次執行到這個coroutine的時候,是把其幀丟到呼叫者的 棧上的。
有棧協程(stackful coroutine)
而Go的協程則屬於有棧協程,在這篇文章裡有說到 具體切換的時候,涉及到哪些東西。我在這裡再列出來一下,我們看看G長啥樣:
type g struct { // Stack parameters. // stack describes the actual stack memory: [stack.lo, stack.hi). // stackguard0 is the stack pointer compared in the Go stack growth prologue. // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. // stackguard1 is the stack pointer compared in the C stack growth prologue. // It is stack.lo+StackGuard on g0 and gsignal stacks. // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash). stackstack// offset known to runtime/cgo stackguard0 uintptr // offset known to liblink stackguard1 uintptr // offset known to liblink _panic*_panic // innermost panic - offset known to liblink _defer*_defer // innermost defer m*m// current m; offset known to arm liblink schedgobuf ... }
其中的gobuf就是協程中儲存的東西,我們看看:
type gobuf struct { // The offsets of sp, pc, and g are known to (hard-coded in) libmach. // // ctxt is unusual with respect to GC: it may be a // heap-allocated funcval, so GC needs to track it, but it // needs to be set and cleared from assembly, where it's // difficult to have write barriers. However, ctxt is really a // saved, live register, and we only ever exchange it between // the real register and the gobuf. Hence, we treat it as a // root during stack scanning, which means assembly that saves // and restores it doesn't need write barriers. It's still // typed as a pointer so that any other writes from Go get // write barriers. spuintptr pcuintptr gguintptr ctxt unsafe.Pointer retsys.Uintreg lruintptr bpuintptr // for GOEXPERIMENT=framepointer }
把好幾個重要的暫存器的值全部儲存了,如 SP, PC等。其實就是把整個G的棧全部儲存下來,換成新的要執行的G的棧的值。
對比
stackful coroutine的優勢在於可以在棧執行到任意一步的時候端走,而stackless則只能端走棧中最頂部的那個frame。 如果要說stackful coroutine的缺點的話,我覺得應該是實現比較複雜,goroutine的實現還算簡單,是因為它沒有用到所有的 暫存器,只用到棧底,棧頂,framepointer,PC等幾個。例如傳引數,都是統一往棧裡丟,當然,它這樣實現也是為了簡單。 一般的C程式,都是前幾個引數丟某某暫存器,更多的引數才丟棧裡。
參考資料:
- ofollow,noindex" target="_blank">https://en.wikipedia.org/wiki/Program_counter
- https://en.wikipedia.org/wiki/Data_segment
- https://en.wikipedia.org/wiki/Call_stack