<wbr id="juant"></wbr>
  • <wbr id="juant"></wbr>
    更多課程 選擇中心


    Python培訓

    400-111-8989

    如何理解 Python 的異步編程?

    • 發布:Python培訓
    • 來源:Python常見問題
    • 時間:2017-07-11 14:12

    異步編程的 Python 實現,以及應用場景。

    和異步編程相對應的是同步編程,我們剛開始學編程的時候一般寫的代碼都是所謂同步的,就是依次執行每一條指令。

    即使加入了條件、循環和函數調用等語法支持,但是從本質上來看,我們認為這種編程方式在同一時間還是只能執行一條指令,完成一個才執行下一個。

    下面舉幾個同步編程的例子:

    - 批處理程序 一般是采用同步的方式來寫:獲取輸入,處理數據,輸出結果。這些步驟就這么一環扣一環地執行下去,別無他求。
    - 命令行程序 一般都很小,主要是為了方便快捷地完成一些轉換。這種任務通常被分解為一系列小任務,然后按指定順序一步一步執行就好了。

    異步程序 和上面提到的這些就不太一樣了。盡管也是按照一定的步驟執行,區別在于,整個系統不需要等待某一個任務執行完畢就可以進行下一步。

    就是說,我們的程序可以在一個(或者多個)任務還在執行過程中就繼續執行下面的步驟。這也意味著當那些(后臺)任務一旦完成,我們還得接管回來繼續處理。

    為什么要這么做呢?簡單來說,因為這種方式可以解決同步編程不好解決或者解決不了的問題。

    下面給大家舉幾個有代表性的異步編程的例子:

    簡化的網絡服務器

    網絡服務器的基本工作流程跟我們剛才提到的批處理程序也差不多,獲取輸入,處理數據,輸出結果。用同步的方式當然也能讓服務器運行起來 —— 以一種很刺激又悲催的方式。

    為什么? 網絡服務器的工作任務可不光是完成單獨的一個小流程(輸入,處理,輸出),這還遠遠不夠,它必須能持久穩定的同時處理成百上千個同樣的工作流程。

    同步網絡服務器還能改進嗎? 我們或許可以通化優化程序執行效率來加快處理速度。但事實上,在網絡服務器上有很多不以我們意志為轉移的限制條件,就注定了它沒法做出快速響應,也沒法同時應對大量的用戶請求。

    到底是哪些限制條件呢? 網絡帶寬,文件讀寫速度,數據庫查詢速度,以及其他相關服務的速度等等。這些限制條件有一個共同點——都是讀寫操作。這類操作的速度比我們的 CPU 速度通常會慢好幾個量級。

    如果我們的網絡服務器是同步模式的,假如執行了一個數據查詢的步驟(打個比方),在查詢結果返回之前的很長一段時間里 CPU 基本都處于閑置狀態,完全可以用來干點別的。

    對于批處理程序來說,這一點并不關鍵,處理讀寫操作的結果才是關鍵,并且耗時也遠比讀寫操作要多得多。對于這種程序,優化的重點就不是讀寫操作部分,而是處理過程。

    其實文件、網絡和數據庫的讀寫也不算慢,只是和 CPU 的速度相比顯得很慢。異步編程技術能夠讓我們充分利用相對較慢的讀寫操作的工作間隙,把 CPU 過剩的能量釋放出來去處理其他任務。

    我自己剛接觸異步的時候,不管是請教別人也好,還是查找資料也好,都遇到同一個問題,他們總是強調代碼的非阻塞性。我覺得這是個誤區。

    非阻塞是什么鬼?阻塞又是什么鬼?我都不知道這項技術的應用環境和具體效果,直接講這些不著邊兒的術語和概念又有什么意義呢?

    真實的世界就是異步的

    異步編程(和常規編程思路)有點區別,很容易懵。但是很有意思,因為我們生活的真實世界,以及我們跟這個世界相處的方式,本身就是異步的。

    你應該有過這樣的經歷: 作為一名家長,有時會同時忙活好幾件事兒,結算賬單,洗衣服,同時還要看孩子。

    我們自己這么做的時候甚至是下意識的,但是現在,我要把它分解開來:

    - 結算賬單是我們需要完成的一項任務,可以視為一個同步的任務,一項一項結算,直到全部算完。
    - 有時,我們算著算著賬就要停一下,去把烘干機里的衣服取出來,再把洗衣機里洗好的衣服扔進去繼續烘,然后洗衣機還要再洗一缸。這些任務完成的過程就是異步的。
    - 洗衣機和烘干機分別都是同步任務,但是我們只需要啟動機器,后面的大部分工作他們可以自主完成,這個時候我們就可以回去繼續算我們的賬了。這時就是異步任務了,洗衣機和烘干機可以獨立工作,等完成了就用蜂鳴器通知我們過去處理。
    - 看孩子也是異步的。一旦開始玩游戲就基本不用管他們了。等到肚子餓了或者碰傷了他們就會哭著喊著找爸爸媽媽,這時我們再去處理。孩子對于我們來說在很長一段時間里都處于高優先級,比其它那些算賬、洗衣服什么的優先級都高。

    這個例子闡述了阻塞和非阻塞兩種情況。我們去洗衣機的時候,CPU (家長)就會處于忙碌狀態,就沒法再同時忙別的事兒了(看孩子)。

    這些活也用不了多少時間,也沒什么問題。我們啟動洗衣機和烘干機之后,就可以回去忙其它事情了,這時洗衣服這個任務就是異步的,因為我們可以不用管它,去做其它事兒,等到它們完成了設定的任務就會用蜂鳴器通知我們。

    作為一個正常人,我們天生就是這樣,自然而然的就會同時兼顧好多事情。對于一個程序員來說,怎么才能把這種行為模式轉化為相應的代碼才是我們要說的重點。

    下面我先介紹一種大家比較熟悉的代碼思路:

    腦洞1:“批處理”家長模式

    想象一下如果是同步模式,要怎么來完成這些任務。作為一名稱職的家長,在這種情況下肯定首先要看好孩子,除非其它事情主動來找我,否則就一直看著孩子。顯然,結賬和洗衣服什么的(肯定不會主動來找我們)就都不用干了。

    我們也可以調整任務的優先級,但是在同步模式下,一次還是只能干一件事兒,然后一個接一個干。這就跟我們上面提到的同步模式的網絡服務器差不多,不是說不能這么干,就是沒什么好下場,誰干誰知道。

    除非孩子們睡下,不然啥也別想干,等他們都睡了,黃花菜都涼了。要是真敢這么干,要不了幾個星期,家長都得瘋。

    腦洞2:“輪詢”家長模式

    我們換個思路,改為輪詢模式。家長定期中斷當前任務,查看一下有沒有其它活還要干。

    開啟這種模式后,我們可以設置每隔 15 分鐘中斷一下。然后就每隔 15 分鐘去看看洗衣機、烘干機或者孩子們哪些需要自己來處理,如果有,就去把那件事兒做掉,如果都相安無事,就回去接著結算賬單,等著下一次輪詢間隔。

    這個方案也不是不行,任務肯定能完成,但是會導致一些問題。一方面,有些任務盡管在一段時間內肯定完不成,仍要占用 CPU (家長)很多不必要的時間,比如洗衣機和烘干機。另一方面,這些任務也有可能在輪詢間隔內完成,然而不能被及時發現和處理,除非等到下一個輪詢周期。一些高優先級的任務,比如看孩子,恐怕會因為這么長的周期而導致一些嚴重的事故。

    我們當然可以通過縮短輪詢間隔來解決這個問題,不過 CPU 就會耗費更多的時間來切換任務,從而降低性能。還是那句老話,要是真敢這么干,要不了幾個星期,家長又得瘋一回。

    腦洞3:“線程”家長模式

    家長都有個口頭禪,”恨不得一個人掰成兩半用“。我們可以用線程在代碼里掰出一大堆家長來。

    如果我們把所有的任務視為一個整體,就可以把任務細化分配到多個線程里,然后給每個任務線程復制一個家長。這樣每個任務都有一個家長去做,看孩子、看烘干機、看洗衣機、做結算,每個任務都獨立完成。聽起來很不錯哦。

    事實真的如此美妙嗎?由于我們要明白的告訴每個家長(CPU)要做的工作,這里面很容易出問題,因為所有的家長在完成任務的過程中會共享所有資源。

    比如說,看著烘干機的家長 A 發現衣服干了,就要把衣服取出來。假如在家長 A 正在取衣服的過程中,另一個看著洗衣機的家長 B 剛好發現洗衣機也洗完了,就打算使用烘干機,這樣才能把衣服從洗衣機里取出來放到烘干機去烘。然后家長 A 取完衣服之后,也打算使用洗衣機,把衣服從洗衣機取出來放到烘干機去烘。

    這時,兩個家長就陷入了僵局(所謂死鎖)。

    兩個人都控制著自己的資源,同時請求控制對方的資源,并且都妄想對方先釋放資源給自己(之后才能釋放自己的資源)。這就是使用線程時程序員們必須要解決的問題。

    還有一個問題也是使用線程時可能遇到的。假設出現了意外,一個孩子受傷了,照看他的家長需要帶他去急診,他處理的很及時,畢竟這個家長是專門負責照看孩子的。但是到了醫院該家長要填一張大面額的支票來付醫療費。

    但是,家里那個負責結算賬目的家長并不知道這張支票的事,于是家里的賬戶就透支了。因為所有的家長實例都運行在同一個程序當中,家里的錢(賬本)是大家共享的,我們必須讓照看孩子的家長及時通知管賬的家長。或者提出一個鎖定機制,確保同一種資源同一個時間只能被一個家長使用和改動。

    這些問題在線程模式下也不是不能解決,只不過比較復雜,最頭疼的是,出了錯也不易察覺。

    Python 實現

    下面我們就要開始將上面這些腦洞方案用 Python 來實現。

    你可以在 Github 代碼庫 下載全部示例。

    所有示例在 Python 3.6.1 下測試通過,示例代碼依賴列表 包含了全部依賴文件。

    強烈建議開啟 Python 虛擬環境 運行示例,以免受系統 Python 環境干擾。

    例1:同步編程

    第1個例子我們設計了一個任務,它要從指定隊列里依次領受并完成一項工作。在這個例子里工作也很簡單,就是從隊列里順次讀取一個數字,然后開始數數,數到這個數就停。同時,每數一次就打印一句話,數完之后打印總共數了幾個數。這個程序提供了一個簡單的多任務處理隊列數據的范例。


    程序里的任務其實就是一個函數,它接收兩個參數,一個字符串,一個隊列。執行的時候先檢查隊列里有沒有要處理的工作(一個數字),如果有,就把它從隊列里取出來,然后開始數數,數到頭了就顯示出來。不斷重復這個過程,一直到把列表里的工作全部做完,然后退出程序。

    程序運行之后我們得到一個詳細的狀態列表,從中可以看到所有工作都是 Task One 完成的。等它忙完了,Task Two 才得以運行,但是到那時列表里的工作也沒了,所以 Task Two 無事可做,只好打印一個狀態聲明就直接退出了。在這段代碼里沒有設置任何機制來幫助這兩個任務和諧地切換和共存。
    例2:簡單的合作并發編程

    這個例子是上例的升級版,加入了 Python 的生成器,這樣就可以讓兩個任務進行。在任務函數執行部分加入了 yield 關鍵字,這意味著循環執行到這一句就會退出,同時保存上下文環境,等到需要的時候再繼續運行。下面代碼中 # run the tasks 的地方就利用了這個特性,使用 t.next() 方法來喚醒之前的任務,讓它在剛才 yield 的地方繼續。

    這是合作并發的一種方式。程序交出當前環境的控制權,讓其它任務得以執行。在本例中,這么做可以讓主程序中同時運行兩個任務,并且共享同一個隊列數據資源。雖然是個不錯的方案,但是想得到和上面例子一樣的運行結果,還存在很多要改進的地方。

    程序運行信息顯示兩個任務確實同時在執行,都從隊列中獲取了數據并處理。這算是我們想要的效果,兩個任務都工作正常,各在隊列里處理了兩個數據。但是我再強調一遍,要實現這個結果還有很多工作要做。

    這個例子實現的技巧是使用了 yield 語句將任務函數轉換為生成器,以便切換的時候保存上下文。也正是利用這一點才實現了兩個不同的任務實例之間的切換。

    例3:協作并發與阻塞調用編程

    再次升級程序,在任務循環的主體部分加入 time.sleep(1) 這個函數,其它部分跟上例一樣。這個函數設置了每次循環中有 1 秒的延遲,以此來模擬日常任務中速度較慢的 IO 操作的效果。

    除此之外我還使用了一個計時器工具,用來在打印運行日志的時候顯示開始和消耗的時間。

    從程序運行狀態來看,跟之前一樣,它也能讓兩個任務同時進行,從隊列里獲取并處理數據。但是,由于模擬了讀寫操作造成的延遲效果,我們可以看到,使用協作并發的方式并沒有給我們帶來什么好處,每一處延遲都拖慢了整個程序執行的進度,高速的 CPU 只能閑在一邊默默的看它龜爬。

    例4:協作并發與非阻塞調用編程(協程)
    下面我們給程序做一個大升級。首先在程序中引入一個叫做 協程異步 的庫。接下來引用它提供的一個叫做 monkey 的模塊。

    該模塊里有一個 patch_all() 方法。這個東西到底是干什么用的呢?簡單來說,有了它就能把其它各種庫里采用阻塞模式(同步模式)運行的代碼統統修補(patch_all 就是統統修補的意思)為異步模式。

    說的太簡單了,有的人可能不太理解。對我們的示例程序來說,就是我們模擬的讀寫操作本來會導致整個程序暫停來等待它執行完畢的,現在有了這個 patch_all() 就不用等了,它能讓系統繼續運行。請注意,上例中的 yield 語句現在已經不寫了。

    再來看,如果 time.sleep(1) 這個函數在協程的作用下不再占用系統控制權,那么控制權現在交給誰了呢?我們使用協程的時候,它會自動開啟一個事件循環的線程。對我們來說就跟之前例3中使用多任務的循環差不多。等到延遲的部分執行完畢后,就接著執行延遲下面的語句。這么做的好處就是 CPU 在延遲的過程中被釋放出來可以去做其它事情,不再需要陪著它干等了。

    我們之前寫的多任務循環也可以刪掉了,任務列表加入兩個 gevent.spawn() 函數。它們負責啟動兩個協程線程(我們稱為 greenlets),這是一種輕量級的微線程,可以用協程的方式來切換上下文,不需要像普通線程那樣由系統來切換。

    接下來,在所有任務都啟動之后我們調用了 gevent.joinall(tasks) 方法,這么做是為了讓程序等待兩個任務全部完成。如果不寫這句,程序就會一直往下執行打印語句,然后就結束了。

    程序運行狀態表明,兩個任務同時啟動了,然后同時等待我們模擬的讀寫延遲。這證實了我們使用的 time.sleep(1) 函數沒有導致整個程序中斷,其它任務的運行沒有受到影響。

    程序結尾的總耗時基本是上例總耗時的一半。這就是異步帶來的優勢。

    有了協程的 greenlets 方法和上下文切換控制,我們就可以采用非阻塞的方式并發運行兩個甚至多個讀寫操作,即使任務之間的轉換比較復雜也能得心應手。

    英文原文:https://dbader.org/blog/understanding-asynchronous-programming-in-python#. 
    譯者:WDatou

    例5:同步(阻塞)模式的 HTTP 下載

    這次升級程序我們要舊瓶裝新酒,一方面加入真正的讀寫任務,按照指定網址列表發起 HTTP 請求并獲取頁面內容,另一方面停止異步模式,退回之前的同步阻塞模式。

    這里要引用 requests 庫 來實現真正的 HTTP 請求,同時將之前的數字列表替換成網址列表。新的任務就不再是數數了,而是要把隊列里的網址內容加載進來,然后顯示耗時。

    這里還是采用之前的 yield方法將任務函數變成生成器,保存上下文以便切換任務。

    每個任務依次從隊列中讀取網址,然后訪問該網址,最后顯示耗時。

    上面的例子中使用 yield 的時候是可以讓多任務一起執行的,但是這次不一樣,每個網絡請求在獲得頁面返回內容之前 CPU 都是阻塞狀態。請留意最下面的總耗時,在下個例子里我們會再提到。

    例6:協程異步(非阻塞)模式的 HTTP 下載

    這次我們把協程的方案也放進來。還記得我剛才說過,使用協程庫里的 monkey.patch_all() 方法可以把任何同步代碼轉換為異步,當然也包括這個任務里用到的 requests 庫。

    現在 requests.get(url) 函數可以通過協程的事件循環來切換上下文,轉換成非阻塞模式了,也就不用寫 yield 了。在任務執行部分,我們使用協程來啟動兩個任務,最后用 joinall() 方法等待它們執行完畢。

    仔細看看結尾的總耗時和每個網址分別耗時,顯爾易見,總耗時小于每個網址的單獨耗時之和。

    因為每個請求都是異步的,這就幫助我們有效的利用了 CPU 資源來同時處理多個請求。

    例7:基于 Twisted 的異步(非阻塞) HTTP 下載

    下面這個例子我們改用 Twisted 框架 來實現剛才協程模塊的任務,用非阻塞模式下載網頁內容。

    Twisted 非常強大,它采用完全不同的方法來創建異步程序。協程用修改模塊的方法將同步轉換為異步,Twisted 則提供了自己的一套函數和方法來實現同樣的效果。

    例6 中是用修補 requests.get(url) 的方式獲取頁面,現在我們改用 Twisted 提供的 getPage(url) 方法。

    在下面的代碼中,裝飾器 @defer.inlineCallbacks 和 yield getPage(url) 一起使用,是為了將上下文切換到 Twisted 的事件循環當中。

    在協程中事件循環是隱式的,而在 Twisted 中是通過程序結尾的 reactor.run() 語句來顯式調用的。

    這個運行結果和前面用協程方法得到的一樣,總耗時小于每個網址訪問單獨耗時的總和。

    例8:基于 Twisted 回調方法的異步(非阻塞)HTTP 下載

    這個例子我仍然使用 Twisted 這個庫,只不過換了一個比較傳統的方式。

    比如說,上面我們采用了 @defer.inlineCallbacks / yield的編碼形式,現在我們要改成顯式回調。所謂回調函數就是一個傳遞給系統的用來響應某個事件函數。下面例子中的 success_callback() 函數就是傳遞給 Twisted 用來響應 getPage(url) 事件的回調函數。

    注意,這個例子里的 my_task() 任務函數已經不再需要 @defer.inlineCallbacks 裝飾器了。另外,這個函數還生成了一個延遲變量,我們簡稱它為 d ,它是 getPage(url) 函數的返回值。

    延遲變量是 Twisted 處理異步編程的方式,也是回調函數所必需的。一旦延遲被“觸發”(也就是getPage(url) 完成時),就立刻調用回調函數,并傳入指定參數。

    程序運行結果和上面兩個示例一樣,總耗時小于每個訪問分別耗時之和。

    你想用協程還是Twisted的方式來實現都可以,因人而異。兩種方案都提供了強大的功能幫助開發者來創建異步代碼。

    結論

    希望以上內容能幫助你了解掌握異步編程的應用場景和工作方式。如果你要計算 Pi 的小數點后 100 萬位,那恐怕異步起不了什么作用。

    然而,如果你要實現一個服務器,或者其它一些需要大量讀寫操作的代碼,那你肯定能感受到什么叫質的飛躍。這項技術非常強大,一定能讓你的程序性能上一個新臺階。

    預約申請免費試聽課

    填寫下面表單即可預約申請免費試聽!怕錢不夠?可就業掙錢后再付學費! 怕學不會?助教全程陪讀,隨時解惑!擔心就業?一地學習,可全國推薦就業!

    上一篇:學python語言以后能干嘛?
    下一篇:Python和C/C++交互有幾種方法?

    Python培訓班線上線下哪種靠譜

    python線上培訓班學費一般多少

    Python線下培訓班有哪些

    一篇文章帶你了解python和c語言的區別

    • 掃碼領取資料

      回復關鍵字:視頻資料

      免費領取 達內課程視頻學習資料

    • 視頻學習QQ群

      添加QQ群:1143617948

      免費領取達內課程視頻學習資料

    Copyright ? 2021 Tedu.cn All Rights Reserved 京ICP備08000853號-56 京公網安備 11010802029508號 達內時代科技集團有限公司 版權所有

    選擇城市和中心
    黑龍江省

    吉林省

    河北省

    湖南省

    貴州省

    云南省

    廣西省

    海南省

    天天日天天射天天干天天伊|奇米电影|奇米网_奇米首页|奇米首页 百度 好搜 搜狗
    <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>