一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

Python 中的進程、線程、協(xié)程、同步、異步、回調(diào)

 icecity1306 2015-07-06

進程和線程究竟是什么東西?傳統(tǒng)網(wǎng)絡(luò)服務(wù)模型是如何工作的?協(xié)程和線程的關(guān)系和區(qū)別有哪些?IO過程在什么時間發(fā)生?

在剛剛結(jié)束的 PyCon2014 上海站,來自七牛云存儲的 Python 高級工程師許智翔帶來了關(guān)于 Python 的分享《Python中的進程、線程、協(xié)程、同步、異步、回調(diào)》。

一、上下文切換技術(shù)

簡述

在進一步之前,讓我們先回顧一下各種上下文切換技術(shù)。

不過首先說明一點術(shù)語。當(dāng)我們說“上下文”的時候,指的是程序在執(zhí)行中的一個狀態(tài)。通常我們會用調(diào)用棧來表示這個狀態(tài)——棧記載了每個調(diào)用層級執(zhí)行到哪里,還有執(zhí)行時的環(huán)境情況等所有有關(guān)的信息。

當(dāng)我們說“上下文切換”的時候,表達的是一種從一個上下文切換到另一個上下文執(zhí)行的技術(shù)。而“調(diào)度”指的是決定哪個上下文可以獲得接下去的CPU時間的方法。

進程

進程是一種古老而典型的上下文系統(tǒng),每個進程有獨立的地址空間,資源句柄,他們互相之間不發(fā)生干擾。

每個進程在內(nèi)核中會有一個數(shù)據(jù)結(jié)構(gòu)進行描述,我們稱其為進程描述符。這些描述符包含了系統(tǒng)管理進程所需的信息,并且放在一個叫做任務(wù)隊列的隊列里面。

很顯然,當(dāng)新建進程時,我們需要分配新的進程描述符,并且分配新的地址空間(和父地址空間的映射保持一致,但是兩者同時進入COW狀態(tài))。這些過程需要一定的開銷。

進程狀態(tài)

忽略去linux內(nèi)核復(fù)雜的狀態(tài)轉(zhuǎn)移表,我們實際上可以把進程狀態(tài)歸結(jié)為三個最主要的狀態(tài):就緒態(tài),運行態(tài),睡眠態(tài)。這就是任何一本系統(tǒng)書上都有的三態(tài)轉(zhuǎn)換圖。

就緒和執(zhí)行可以互相轉(zhuǎn)換,基本這就是調(diào)度的過程。而當(dāng)執(zhí)行態(tài)程序需要等待某些條件(最典型就是IO)時,就會陷入睡眠態(tài)。而條件達成后,一般會自動進入就緒。

阻塞

當(dāng)進程需要在某個文件句柄上做IO,這個fd又沒有數(shù)據(jù)給他的時候,就會發(fā)生阻塞。具體來說,就是記錄XX進程阻塞在了XX fd上,然后將進程標(biāo)記為睡眠態(tài),并調(diào)度出去。當(dāng)fd上有數(shù)據(jù)時(例如對端發(fā)送的數(shù)據(jù)到達),就會喚醒阻塞在fd上的進程。進程會隨后進入就緒隊列,等待合適的時間被調(diào)度。

阻塞后的喚醒也是一個很有意思的話題。當(dāng)多個上下文阻塞在一個fd上(雖然不多見,但是后面可以看到一個例子),而且fd就緒時,應(yīng)該喚醒多少個上下文呢?傳統(tǒng)上應(yīng)當(dāng)喚醒所有上下文,因為如果僅喚醒一個,而這個上下文又不能消費所有數(shù)據(jù)時,就會使得其他上下文處于無謂的死鎖中。

但是有個著名的例子——accept,也是使用讀就緒來表示收到的。如果試圖用多個線程來accept會發(fā)生什么?當(dāng)有新連接時,所有上下文都會就緒,但是只有第一個可以實際獲得fd,其他的被調(diào)度后又立刻阻塞。這就是驚群問題thundering herd problem。

現(xiàn)代linux內(nèi)核已經(jīng)解決了這個問題,方法驚人的簡單——accept方法加鎖。

(inet_connection_sock.c:inet_csk_wait_for_connect)

線程

線程是一種輕量進程,實際上在linux內(nèi)核中,兩者幾乎沒有差別,除了一點——線程并不產(chǎn)生新的地址空間和資源描述符表,而是復(fù)用父進程的。
但是無論如何,線程的調(diào)度和進程一樣,必須陷入內(nèi)核態(tài)。

二、傳統(tǒng)網(wǎng)絡(luò)服務(wù)模型

進程模型

為每個客戶分配一個進程。優(yōu)點是業(yè)務(wù)隔離,在一個進程中出現(xiàn)的錯誤不至于影響整個系統(tǒng),甚至其他進程。Oracle傳統(tǒng)上就是進程模型。缺點是進程的分配和釋放有非常高的成本。因此Oracle需要連接池來保持連接減少新建和釋放,同時盡量復(fù)用連接而不是隨意的新建連接。

線程模型

為每客戶分配一個線程。優(yōu)點是更輕量,建立和釋放速度更快,而且多個上下文間的通訊速度非常快。缺點是一個線程出現(xiàn)問題容易將整個系統(tǒng)搞崩潰。

一個例子

py_http_fork_thread.py

在這個例子中,線程模式和進程模式可以輕易的互換。

如何工作的:

  1. 父進程監(jiān)聽服務(wù)端口
  2. 在有新連接建立的時候,父進程執(zhí)行fork,產(chǎn)生一個子進程副本
  3. 如果子進程需要的話,可以exec(例如CGI)
  4. 父進程執(zhí)行(理論上應(yīng)當(dāng)先執(zhí)行子進程,因為exec執(zhí)行的快可以避免COW)到accept后,發(fā)生阻塞
  5. 上下文調(diào)度,內(nèi)核調(diào)度器選擇下一個上下文,如無意外,應(yīng)當(dāng)就是剛剛派生的子進程
  6. 子進程進程進入讀取處理狀態(tài),阻塞在read調(diào)用上,所有上下文均進入睡眠態(tài)
  7. 隨著SYN或者數(shù)據(jù)報文到來,CPU會喚醒對應(yīng)fd上阻塞的上下文(wait_queue),切換到就緒態(tài),并加入調(diào)度隊列
  8. 上下文繼續(xù)執(zhí)行到下一個阻塞調(diào)用,或者因為時間片耗盡被掛起

評價

  • 同步模型,編寫自然,每個上下文可以當(dāng)作其他上下文不存在一樣的操作,每次讀取數(shù)據(jù)可以當(dāng)作必然能讀取到。
  • 進程模型自然的隔離了連接。即使程序復(fù)雜且易崩潰,也只影響一個連接而不是在整個系統(tǒng)。
  • 生成和釋放開銷很大(效率測試的進程fork和線程模式開銷測試),需要考慮復(fù)用。
  • 進程模式的多客戶通訊比較麻煩,尤其在共享大量數(shù)據(jù)的時候。

性能

thread模式,虛擬機:

1: 909.27 2: 3778.38 3: 4815.37 4: 5000.04 10: 4998.16 50: 4881.93 100: 4603.24 200: 3445.12 500: 1778.26 (出現(xiàn)錯誤)

fork模式,虛擬機:

1: 384.14 2: 435.67 3: 435.17 4: 437.54 10: 383.11 50: 364.03 100: 320.51 (出現(xiàn)錯誤)

thread模式,物理機:

1: 6942.78 2: 6891.23 3: 6584.38 4: 6517.23 10: 6178.50 50: 4926.91 100: 2377.77

注意在python中,雖然有GIL,但是一個線程陷入到網(wǎng)絡(luò)IO的時候,GIL是解鎖的。因此從調(diào)用開始到調(diào)用結(jié)束,減去CPU切換到其他上下文的時間,是可以多線程的?,F(xiàn)象是,在此種狀況下可以觀測到短暫的python CPU用量超過100%。

如果執(zhí)行多個上下文,可以充分利用這段時間。所觀測到的結(jié)果就是,只能單核的python,在小范圍內(nèi),其隨著并發(fā)數(shù)上升,性能居然會跟著上升。如果將這個過程轉(zhuǎn)移到一臺物理機上執(zhí)行,那么基本不能得出這樣的結(jié)論。這主要是因為虛擬機上內(nèi)核陷入的開銷更高。

三、C10K 問題

描述

當(dāng)同時連接數(shù)在10K左右時,傳統(tǒng)模型就不再適用。實際上在效率測試報告的線程切換開銷一節(jié)可以看到,超過1K后性能就差的一塌糊涂了。

進程模型的問題:

在C10K的時候,啟動和關(guān)閉這么多進程是不可接受的開銷。事實上單純的進程fork模型在C1K時就應(yīng)當(dāng)拋棄了。

Apache的prefork模型,是使用預(yù)先分配(pre)的進程池。這些進程是被復(fù)用的。但即便是復(fù)用,本文所描述的很多問題仍不可避免。

線程模式的問題

從任何測試都可以表明,線程模式比進程模式更耐久一些,性能更好。但是在面對C10K還是力不從心的。問題是,線程模式的問題出在哪里呢?

內(nèi)存?

有些人可能認(rèn)為線程模型的失敗首先在于內(nèi)存。如果你這么認(rèn)為,一定是因為你查閱了非常老的資料,并且沒仔細(xì)思考過。

你可能看到資料說,一個線程棧會消耗8M內(nèi)存(linux默認(rèn)值,ulimit可以看到),512個線程棧就會消耗4G內(nèi)存,而10K個線程就是80G。所以首先要考慮調(diào)整棧深度,并考慮爆棧問題。

聽起來很有道理,問題是——linux的棧是通過缺頁來分配內(nèi)存的(How does stack allocation work in Linux?),不是所有棧地址空間都分配了內(nèi)存。因此,8M是最大消耗,實際的內(nèi)存消耗只會略大于實際需要的內(nèi)存(內(nèi)部損耗,每個在4k以內(nèi))。但是內(nèi)存一旦被分配,就很難回收(除非線程結(jié)束),這是線程模式的缺陷。

這個問題提出的前提是,32位下地址空間有限。雖然10K個線程不一定會耗盡內(nèi)存,但是512個線程一定會耗盡地址空間。然而這個問題對于目前已經(jīng)成為主流的64位系統(tǒng)來說根本不存在。

內(nèi)核陷入開銷?

所謂內(nèi)核陷入開銷,就是指CPU從非特權(quán)轉(zhuǎn)向特權(quán),并且做輸入檢查的一些開銷。這些開銷在不同的系統(tǒng)上差異很大。

線程模型主要通過陷入切換上下文,因此陷入開銷大聽起來有點道理。實際上,這也是不成立的。線程在什么時候發(fā)生陷入切換?正常情況下,應(yīng)當(dāng)是IO阻塞的時候。同樣的IO量,難道其他模型就不需要陷入了么?只是非阻塞模型有很大可能直接返回,并不發(fā)生上下文切換而已。

效率測試報告的基礎(chǔ)調(diào)用開銷一節(jié),證實了當(dāng)代操作系統(tǒng)上內(nèi)核陷入開銷是非常驚人的小的(10個時鐘周期這個量級)。

線程模型的問題在于切換成本高

熟悉linux內(nèi)核的應(yīng)該知道,近代linux調(diào)度器經(jīng)過幾個階段的發(fā)展。

  1. linux2.4的調(diào)度器
  2. O(1)調(diào)度器
  3. CFS

實際上直到O(1),調(diào)度器的調(diào)度復(fù)雜度才和隊列長度無關(guān)。在此之前,過多的線程會使得開銷隨著線程數(shù)增長(不保證線性)。

O(1)調(diào)度器看起來似乎是完全不隨著線程的影響。但是這個調(diào)度器有顯著的缺點——難于理解和維護,并且在一些情況下會導(dǎo)致交互式程序響應(yīng)緩慢。
CFS使用紅黑樹管理就緒隊列。每次調(diào)度,上下文狀態(tài)轉(zhuǎn)換,都會查詢或者變更紅黑樹。紅黑樹的開銷大約是O(logm),其中m大約為活躍上下文數(shù)(準(zhǔn)確的說是同優(yōu)先級上下文數(shù)),大約和活躍的客戶數(shù)相當(dāng)。

因此,每當(dāng)線程試圖讀寫網(wǎng)絡(luò),并遇到阻塞時,都會發(fā)生O(logm)級別的開銷。而且每次收到報文,喚醒阻塞在fd上的上下文時,同樣要付出O(logm)級別的開銷。

分析

O(logm)的開銷看似并不大,但是卻是一個無法接受的開銷。因為IO阻塞是一個經(jīng)常發(fā)生的事情。每次IO阻塞,都會發(fā)生開銷。而且決定活躍線程數(shù)的是用戶,這不是我們可控制的。更糟糕的是,當(dāng)性能下降,響應(yīng)速度下降時。同樣的用戶數(shù)下,活躍上下文會上升(因為響應(yīng)變慢了)。這會進一步拉低性能。

問題的關(guān)鍵在于,http服務(wù)并不需要對每個用戶完全公平,偶爾某個用戶的響應(yīng)時間大大的延長了是可以接受的。在這種情況下,使用紅黑樹去組織待處理fd列表(其實是上下文列表),并且反復(fù)計算和調(diào)度,是無謂的開銷。

四、多路復(fù)用

簡述

要突破C10K問題,必須減少系統(tǒng)內(nèi)活躍上下文數(shù)(其實未必,例如換一個調(diào)度器,例如使用RT的SCHED_RR),因此就要求一個上下文同時處理多個鏈接。而要做到這點,就必須在每次系統(tǒng)調(diào)用讀取或?qū)懭霐?shù)據(jù)時立刻返回。否則上下文持續(xù)阻塞在調(diào)用上,如何能夠復(fù)用?這要求fd處于非阻塞狀態(tài),或者數(shù)據(jù)就緒。

上文所說的所有IO操作,其實都特指了他的阻塞版本。所謂阻塞,就是上下文在IO調(diào)用上等待直到有合適的數(shù)據(jù)為止。這種模式給人一種“只要讀取數(shù)據(jù)就必定能讀到”的感覺。而非阻塞調(diào)用,就是上下文立刻返回。如果有數(shù)據(jù),帶回數(shù)據(jù)。如果沒有數(shù)據(jù),帶回錯誤(EAGAIN)。因此,“雖然發(fā)生錯誤,但是不代表出錯”。

但是即使有了非阻塞模式,依然繞不過就緒通知問題。如果沒有合適的就緒通知技術(shù),我們只能在多個fd中盲目的重試,直到碰巧讀到一個就緒的fd為止。這個效率之差可想而知。

在就緒通知技術(shù)上,有兩種大的模式——就緒事件通知和異步IO。其差別簡要來說有兩點。就緒通知維護一個狀態(tài),由用戶讀取。而異步IO由系統(tǒng)調(diào)用用戶的回調(diào)函數(shù)。就緒通知在數(shù)據(jù)就緒時就生效,而異步IO直到數(shù)據(jù)IO完成才發(fā)生回調(diào)。

linux下的主流方案一直是就緒通知,其內(nèi)核態(tài)異步IO方案甚至沒有被封裝到glibc里去。圍繞就緒通知,linux總共提出過三種解決方案。我們繞過select和poll方案,看看epoll方案的特性。

另外提一點。有趣的是,當(dāng)使用了epoll后(更準(zhǔn)確說只有在LT模式下),fd是否為非阻塞其實已經(jīng)不重要了。因為epoll保證每次去讀取的時候都能讀到數(shù)據(jù),因此不會阻塞在調(diào)用上。

epoll

用戶可以新建一個epoll文件句柄,并且將其他fd和這個"epoll fd"關(guān)聯(lián)。此后可以通過epoll fd讀取到所有就緒的文件句柄。

epoll有兩大模式,ET和LT。LT模式下,每次讀取就緒句柄都會讀取出完整的就緒句柄。而ET模式下,只給出上次到這次調(diào)用間新就緒的句柄。換個說法,如果ET模式下某次讀取出了一個句柄,這個句柄從未被讀取完過——也就是從沒有從就緒變?yōu)槲淳途w。那么這個句柄就永遠不會被新的調(diào)用返回,哪怕上面其實充滿了數(shù)據(jù)——因為句柄無法經(jīng)歷從非就緒變?yōu)榫途w的過程。

類似CFS,epoll也使用了紅黑樹——不過是用于組織加入epoll的所有fd。epoll的就緒列表使用的是雙向隊列。這方便系統(tǒng)將某個fd加入隊列中,或者從隊列中解除。

要進一步了解epoll的具體實現(xiàn),可以參考這篇linux下poll和epoll內(nèi)核源碼剖析。

性能

如果使用非阻塞函數(shù),就不存在阻塞IO導(dǎo)致上下文切換了,而是變?yōu)闀r間片耗盡被搶占(大部分情況下如此),因此讀寫的額外開銷被消除。而epoll的常規(guī)操作,都是O(1)量級的。而epoll wait的復(fù)制動作,則和當(dāng)前需要返回的fd數(shù)有關(guān)(在LT模式下幾乎就等同于上面的m,而ET模式下則會大大減少)。

但是epoll存在一點細(xì)節(jié)問題。epoll fd的管理使用紅黑樹,因此在加入和刪除時需要O(logn)復(fù)雜度(n為總連接數(shù)),而且關(guān)聯(lián)操作還必須每個fd調(diào)用一次。因此在大連接量下頻繁建立和關(guān)閉連接仍然有一定性能問題(超短連接)。不過關(guān)聯(lián)操作調(diào)用畢竟比較少。如果確實是超短連接,tcp連接和釋放開銷就很難接受了,所以對總體性能影響不大。

固有缺陷

原理上說,epoll實現(xiàn)了一個wait_queue的回調(diào)函數(shù),因此原理上可以監(jiān)聽任何能夠激活wait_queue的對象。但是epoll的最大問題是無法用于普通文件,因為普通文件始終是就緒的——雖然在讀取的時候不是這樣。

這導(dǎo)致基于epoll的各種方案,一旦讀到普通文件上下文仍然會阻塞。golang為了解決這個問題,在每次調(diào)用syscall的時候,會獨立的啟動一個線程,在獨立的線程中進行調(diào)用。因此golang在IO普通文件的時候網(wǎng)絡(luò)不會阻塞。

五、事件通知機制下的幾種程序設(shè)計模型

簡述

使用通知機制的一大缺憾就是,用戶進行IO操作后會陷入茫然——IO沒有完成,所以當(dāng)前上下文不能繼續(xù)執(zhí)行。但是由于復(fù)用線程的要求,當(dāng)前線程還需要接著執(zhí)行。所以,在如何進行異步編程上,又分化出數(shù)種方案。

用戶態(tài)調(diào)度

首先需要知道的一點就是,異步編程大多數(shù)情況下都伴隨著用戶態(tài)調(diào)度問題——即使不使用上下文技術(shù)。

因為系統(tǒng)不會自動根據(jù)fd的阻塞狀況來喚醒合適的上下文了,所以這個工作必須由其他人——一般就是某種框架——來完成。

你可以想像一個fd映射到對象的大map表,當(dāng)我們從epoll中得知某個fd就緒后,需要喚醒某種對象,讓他處理fd對應(yīng)的數(shù)據(jù)。

當(dāng)然,實際情況會更加復(fù)雜一些。原則上所有不占用CPU時間的等待都需要中斷執(zhí)行,陷入睡眠,并且交由某種機構(gòu)管理,等待合適的機會被喚醒。例如sleep,或是文件IO,還有l(wèi)ock。更精確的說,所有在內(nèi)核里面涉及到wait_queue的,在框架里面都需要做這種機制——也就是把內(nèi)核的調(diào)度和等待搬到用戶態(tài)來。

當(dāng)然,其實也有反過來的方案——就是把程序扔到內(nèi)核里面去。其中最著名的實例大概是微軟的http服務(wù)器了。

這個所謂的“可喚醒可中斷對象”,用的最多的就是協(xié)程。

協(xié)程

協(xié)程是一種編程組件,可以在不陷入內(nèi)核的情況進行上下文切換。如此一來,我們就可以把協(xié)程上下文對象關(guān)聯(lián)到fd,讓fd就緒后協(xié)程恢復(fù)執(zhí)行。
當(dāng)然,由于當(dāng)前地址空間和資源描述符的切換無論如何需要內(nèi)核完成,因此協(xié)程所能調(diào)度的,只有在同一進程中的不同上下文而已。

如何做到

這是如何做到的呢?

我們在內(nèi)核里實行上下文切換的時候,其實是將當(dāng)前所有寄存器保存到內(nèi)存中,然后從另一塊內(nèi)存中載入另一組已經(jīng)被保存的寄存器。對于圖靈機來說,當(dāng)前狀態(tài)寄存器意味著機器狀態(tài)——也就是整個上下文。其余內(nèi)容,包括棧上內(nèi)存,堆上對象,都是直接或者間接的通過寄存器來訪問的。

但是請仔細(xì)想想,寄存器更換這種事情,似乎不需要進入內(nèi)核態(tài)么。事實上我們在用戶態(tài)切換的時候,就是用了類似方案。

C coroutine的實現(xiàn),基本大多是保存現(xiàn)場和恢復(fù)之類的過程。python則是保存當(dāng)前thread的top frame(greenlet)。

但是非常悲劇的,純用戶態(tài)方案(setjmp/longjmp)在多數(shù)系統(tǒng)上執(zhí)行的效率很高,但是并不是為了協(xié)程而設(shè)計的。setjmp并沒有拷貝整個棧(大多數(shù)的coroutine方案也不應(yīng)該這么做),而是只保存了寄存器狀態(tài)。這導(dǎo)致新的寄存器狀態(tài)和老寄存器狀態(tài)共享了同一個棧,從而在執(zhí)行時互相破壞。而完整的coroutine方案應(yīng)當(dāng)在特定時刻新建一個棧。

而比較好的方案(makecontext/swapcontext)則需要進入內(nèi)核(sigprocmask),這導(dǎo)致整個調(diào)用的性能非常低。

協(xié)程與線程的關(guān)系

首先我們可以明確,協(xié)程不能調(diào)度其他進程中的上下文。而后,每個協(xié)程要獲得CPU,都必須在線程中執(zhí)行。因此,協(xié)程所能利用的CPU數(shù)量,和用于處理協(xié)程的線程數(shù)量直接相關(guān)。

作為推論,在單個線程中執(zhí)行的協(xié)程,可以視為單線程應(yīng)用。這些協(xié)程,在未執(zhí)行到特定位置(基本就是阻塞操作)前,是不會被搶占,也不會和其他CPU上的上下文發(fā)生同步問題的。因此,一段協(xié)程代碼,中間沒有可能導(dǎo)致阻塞的調(diào)用,執(zhí)行在單個線程中。那么這段內(nèi)容可以被視為同步的。

我們經(jīng)常可以看到某些協(xié)程應(yīng)用,一啟動就是數(shù)個進程。這并不是跨進程調(diào)度協(xié)程。一般來說,這是將一大群fd分給多個進程,每個進程自己再做fd-協(xié)程對應(yīng)調(diào)度。

基于就緒通知的協(xié)程框架

  1. 首先需要包裝read/write,在發(fā)生read的時候檢查返回。如果是EAGAIN,那么將當(dāng)前協(xié)程標(biāo)記為阻塞在對應(yīng)fd上,然后執(zhí)行調(diào)度函數(shù)。
  2. 調(diào)度函數(shù)需要執(zhí)行epoll(或者從上次的返回結(jié)果緩存中取數(shù)據(jù),減少內(nèi)核陷入次數(shù)),從中讀取一個就緒的fd。如果沒有,上下文應(yīng)當(dāng)被阻塞到至少有一個fd就緒。
  3. 查找這個fd對應(yīng)的協(xié)程上下文對象,并調(diào)度過去。
  4. 當(dāng)某個協(xié)程被調(diào)度到時,他多半應(yīng)當(dāng)在調(diào)度器返回的路上——也就是read/write讀不到數(shù)據(jù)的時候。因此應(yīng)當(dāng)再重試讀取,失敗的話返回1。
  5. 如果讀取到數(shù)據(jù)了,直接返回。

這樣,異步的數(shù)據(jù)讀寫動作,在我們的想像中就可以變?yōu)橥降?。而我們知道同步模型會極大降低我們的編程負(fù)擔(dān)。

CPS模型

其實這個模型有個更流行的名字——回調(diào)模型。之所以扯上CPS這么高大上的玩意,主要是里面涉及不少有趣的話題。

首先是回調(diào)模型的大致過程。在IO調(diào)用的時候,同時傳入一個函數(shù),作為返回函數(shù)。當(dāng)IO結(jié)束時,調(diào)用傳入的函數(shù)來處理下面的流程。這個模型聽起來挺簡單的。

然后是CPS。用一句話來描述這個模型——他把一切操作都當(dāng)作了IO,無論干什么,結(jié)果要通過回調(diào)函數(shù)來返回。從這個角度來說,IO回調(diào)模型只能被視作CPS的一個特例。

例如,我們需要計算1+2*3,在cps里面就需要這么寫:

mul(lambda x: add(pprint.pprint, x, 1), 2, 3)

其中mul和add在python里面如下定義:

add = lambda f, *nums: f(sum(nums))
mul = lambda f, *nums: f(reduce(lambda x,y: x*y, nums))

而且由于python沒有TCO,所以這樣的寫法會產(chǎn)生非常多的frame。

但是要正確理解這個模型,你需要仔細(xì)思考一下以下幾個問題:

  1. 函數(shù)的調(diào)用過程為什么必須是一個棧?
  2. IO過程在什么時間發(fā)生?調(diào)用發(fā)生時,還是回調(diào)時?
  3. 回調(diào)函數(shù)從哪里調(diào)用?如果當(dāng)時利用工具去看上下文的話,調(diào)用棧是什么樣子的?

函數(shù)組件和返回值

不知道你是否思考過為什么函數(shù)調(diào)用層級(上下文棧)會被表述為一個?!欠裼惺裁幢匾?,必須將函數(shù)調(diào)用的過程定義為一個棧呢?

原因就是返回值和同步順序。對于大部分函數(shù),我們需要得到函數(shù)計算的返回值。而要得到返回值,調(diào)用者就必須阻塞直到被調(diào)用者返回為止。因此調(diào)用者的執(zhí)行狀態(tài)就必須被保存,等到被調(diào)用者返回后繼續(xù)——從這點來說,調(diào)用其實是最樸素的上下文切換手段。而對于少部分無需返回的函數(shù),我們又往往需要他的順序外部效應(yīng)——例如干掉了某個進程,開了一個燈,或者僅僅是在環(huán)境變量里面添加了一項內(nèi)容。而順序外部效應(yīng)同樣需要等待被調(diào)用者返回以表明這個外部效應(yīng)已經(jīng)發(fā)生。

那么,如果我們不需要返回值也不需要順序的外部效應(yīng)呢?例如啟動一個背景程序?qū)?shù)據(jù)發(fā)送到對端,無需保證發(fā)送成功的情況下?;蛘呤情_始一個數(shù)據(jù)抓取行為,無需保證抓取的成功。

通常這種需求我們就湊合著用一個同步調(diào)用混過去了——反正問題也不嚴(yán)重。但是對于阻塞相當(dāng)嚴(yán)重的情況而言,很多人還是會考慮到將這個行為做成異步過程。目前最流行的異步調(diào)用分解工具就是mq——不僅異步,而且分布。當(dāng)然,還有一個更簡單的非分布方案——開一個coroutine。

而CPS則是另一個方向——函數(shù)的返回值可以不返回調(diào)用者,而是返回給第三者。

IO 過程在什么時間發(fā)生

其實這個問題的核心在于——整個回調(diào)模型是基于多路復(fù)用的還是基于異步IO的?

原則上兩者都可以。你可以監(jiān)聽fd就緒,也可以監(jiān)聽IO完成。當(dāng)然,即使監(jiān)聽IO完成,也不代表使用了內(nèi)核態(tài)異步接口。很可能只是用epoll封裝的而已。

回調(diào)函數(shù)的上下文環(huán)境

這個問題則需要和上面提到的“用戶態(tài)調(diào)度框架”結(jié)合起來說。IO回調(diào)注冊的實質(zhì)是將回調(diào)函數(shù)綁定到某個fd上——就如同將coroutine綁定上去那樣。只是coroutine允許你順序的執(zhí)行,而callback則會切碎函數(shù)。當(dāng)然,大部分實現(xiàn)中,使用callback也有好處——coroutine的最小切換開銷也在50ns,而call本身則只有2ns。

狀態(tài)機模型

狀態(tài)機模型是一個更難于理解和編程的模型,其本質(zhì)是每次重入。

想像你是一個周期失憶的病人(就像“一周的朋友”那樣)。那么你如何才能完成一項需要跨越周期的工作呢?例如刺繡,種植作物,或者——交一個男朋友。

當(dāng)然,類比到失憶病人的例子上必須有一點限制。正常的生活技能,還有一些常識性的東西必須不能在周期失憶范圍內(nèi)。例如重新學(xué)習(xí)認(rèn)字什么的可沒人受的了。

答案就是——做筆記。每次重復(fù)失憶后,你需要閱讀自己的筆記,觀察上次做到哪個步驟,下一個步驟是什么。這需要將一個工作分解為很多步驟,在每個步驟內(nèi)“重入”直到步驟完成,轉(zhuǎn)移到下一個狀態(tài)。

同理,在狀態(tài)機模型解法里,每次執(zhí)行都需要推演合適的狀態(tài),直到工作完成。這個模型已經(jīng)很少用到了,因為相比回調(diào)函數(shù)來說,狀態(tài)機模型更難理解和使用,性能差異也不大。

最后順帶一提,交一個男友的方案和其他幾個略有不同,主要靠顏好高冷反差萌,一般人就不要嘗試挑戰(zhàn)了。。。當(dāng)然一般人也不會一周失憶一次,畢竟生活不是韓劇也不是日本動漫。。。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    男女午夜视频在线观看免费| 日本免费一级黄色录像| 国产免费无遮挡精品视频| 夫妻激情视频一区二区三区| 青青操在线视频精品视频| 午夜小视频成人免费看| 国产成人av在线免播放观看av| 成年人免费看国产视频| 国产在线小视频你懂的| 麻豆精品视频一二三区| 日韩欧美91在线视频| 亚洲色图欧美另类人妻| 亚洲伊人久久精品国产| 亚洲成人黄色一级大片| 一区二区欧美另类稀缺| 欧美一区二区三区喷汁尤物| 中文日韩精品视频在线| 日本不卡在线一区二区三区| 99久久国产精品免费| 国产大屁股喷水在线观看视频| 欧美日韩一区二区午夜| 国产传媒精品视频一区| 精品老司机视频在线观看| av免费视屏在线观看| 麻豆精品视频一二三区| 亚洲一区二区欧美在线| 亚洲欧美一二区日韩高清在线| 欧美人妻免费一区二区三区| 国语久精品在视频在线观看| 东京热电东京热一区二区三区| 青青操视频在线观看国产| 久久精品国产亚洲av久按摩| 亚洲中文字幕在线综合视频| 国产肥妇一区二区熟女精品| 亚洲香艳网久久五月婷婷| 中文字幕日韩欧美亚洲午夜| 日韩黄片大全免费在线看| 国产色一区二区三区精品视频| 精品国产亚洲一区二区三区| 欧美精品激情视频一区| 国产又猛又黄又粗又爽无遮挡|