python中的協程(yield)內部是怎麼實現的?python和lua在yield的實現原理上有什麼區別?

大致查了一下,python的yield貌似是用底層虛擬機的棧狀態切換來實現,看了下lua的programming in lua那本書,裡面的coroutine貌似是用線程來切換。是否可以理解為python的coroutine是用單線程的方式,而lua是用多線程方式來實現協程的?

書裡面是這麼說的。

========================================================================

根據大家提供的信息查了下資料,貌似協程的實現跟多線程沒什麼關係,主要還是通過類似保存棧的上下文環境(或者類似的技術)來實現的。 書中說「每一個協同等同於一個線程」,大概只是類比一下,並不是說內部實現是通過多線程來實現的。

很顯然協程不能利用多核的優勢,那麼用這種方式寫程序的好處是什麼?


  • python 的 generator 只保留棧幀上下文,不保留調用棧,而且 generator 函數不允許 return;
  • lua 的 coroutine 保留整個調用棧的上下文;


從 @fleuria 的答案說開:

  • python 的 generator 只保留棧幀上下文,不保留調用棧,而且 generator 函數不允許 return;
  • lua 的 coroutine 保留整個調用棧的上下文;

Python 2.x 的 generator 的實現方式是 Lua 5.1 coroutine 在涉及 Lua/C mixed code 時候的 yield 方式。所以在 Lua 5.1 的時候是不能 yield-from-C 的。不過 Lua 從一開始就可以 resume-from-C,這是因為 Lua 借用了 C runtime stack 作為 coroutine 的 scheduler(具體細節參見《Lua 5.0 Implementation》)。

從 Lua 5.2 開始,在 Lua C API 里引入了用顯示聲明 continuation function 的方式來實現 yield-from-C。

Python 3.x 的 generator 借鑒了 Lua 5.2 的 yield-from-C 形式,可以有限的保留調用棧上下文。

所以 Python 3.x 的純 Python code coroutine 水平才相當於 Lua 5.2 的 Lua/C mixed code coroutine。Python 2.x 的 coroutine 和 Lua 的 coroutine 實現相差更遠。

如果你在只有 ANSI C compiler 的平台上做 script/C hybrid 開發,Lua coroutine 是唯一的選擇了。


大部分語言為了偷工減料以空間換時間,都是每個線程上跑幾個協程,每個協程有自己的一個stack,然後自己來模擬一部分的調度工作。就連C++17的await也是打算這麼乾的。

C#就不是,他能知道你幾個協程是互相通訊的,然後編譯器幫你從頭寫了代碼,把它們都寫成普通的代碼了。


如果你有一個函數叫Perm(list, n)生成list的全排列,n==1時yield list,否則遞歸。

可能某個yield下調用棧是這樣:

main()-&>resume(Perm)-&>Perm(list, 3)-&>Perm(list,2)-&>Perm(list,1)-&>yield list

對於Lua來說,程序會從yield直接跳到resume,等下次resume再回到Perm(list, 1)裡面yield。

對於Python來說,程序會一層一層往下走,從1到2到3到resume(Python里實際是next,性質基本一致),等到下次resume,再一步一步爬回去。


我當一個板磚工,是 @廖雪峰 寫的關於yield的認識,我感覺很好,可以看一下。Python yield 使用淺析


進程/線程切換需要在內核完成,協程不需要,更加輕量,速度更快。在重 I/O的程序里有很大的優勢。比如爬蟲里,開幾百個線程會明顯拖慢速度,但是開協程不會。


lua裡面:當resume的時候,就切換lua_state環境,然後setjmp,緊接著由於pc指向新地址,所以會直接跳轉到該位置;

當yield時,直接回復環境,然後longjmp到該resume點

lua with C:當在C函數內入yield時,會恢復環境,longjmp到resume點,之後再次resume的時候,會因為環境被破壞,導致resume出錯,此時lua會調用k系列函數,讓resume繼續下去


lua底層實現上還是單線程的,只是lua中的每個coroutine都有一個自帶的調用棧。只是你手動調用時底層線程的指針指向了coroutine的棧,退出時指回原來的地方,俗稱還原現場。大概是這樣的,沒深入了解過lua,逃。 或者可以搜一下「纖程」,更或者搜「c++實現協程」等等,這樣你對底層的了解會更多。


對Python來說,題主可以了解一下CPython和Stackless Python。

大致查了一下,python的yield貌似是用底層虛擬機的棧狀態切換來實現,看了下lua的programming in lua那本書,裡面的coroutine貌似是用線程來切換。是否可以理解為python的coroutine是用單線程的方式,而lua是用多線程方式來實現協程的?

題主指的應該是CPython,CPython的yield實現是基於棧和Frame的,Frame是一個CPython虛擬機中的一個模擬棧幀的對象。yield對應CPython中的一個生成器對象,文件在這裡:

cpython: b3f4616b9a94 Objects/genobject.c

yield在虛擬機中對應的是一個操作碼,相應會執行一串CPython虛擬機位元組碼,如果明白yield會生成一個對象,那麼其實很好理解,一個對象的狀態保存和切換使用一些屬性來做,在虛擬機中很好實現。CPython的yield的確是單線程,或者說,其實CPython把yield和對應的生成器只是轉化為一段位元組碼,CPython虛擬機的位元組碼執行是單線程的。

Lua的實現更類似於很多流行的協程庫的實現,使用C中的setjmp和longjmp,使用一個調度器調度線程組,保存和切換上下文,大部分協程的實現,其實就是實現一個線程調度器。


推薦閱讀:

如何向不懂 Python 的人介紹 Python?
Mac下搞 Python 開發用什麼 IDE?
有專門關於Python圖形化界面編程的書嗎?
如何優雅的閱讀openstack源代碼?
python內置的hash函數對於字元串來說,每次得到的值不一樣?

TAG:Python | Lua | 多線程 | 協程 |