python基礎---進程、線程、協程

pyhon—-進程線程、與協程基礎概述

什麼是進程?

進程,是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。前面的話我也沒懂,用非官方的白話來解釋就是——執行中的程序是進程,比如qq不是進程,但是當我們雙擊qq開始使用它的時候,它就變成了一個進程。我們寫的python程序,只有當我們執行它的時候,它纔是進程。我們正在執行的IE瀏覽器,QQ,pycharm都是進程,從操作系統的角度來講,每一個進程都有它自己的內存空間,進程之間的內存是獨立的。

什麼是線程?

線程,有時被稱爲輕量級進程,是程序執行流的最小單元。我們可以理解爲,線程是屬於進程的,我們平時寫的簡單程序,是單線程的,多線程和單線程的區別在於多線程可以同時處理多個任務,這時候我們可以理解爲多線程和多進程是一樣的,我可以在我的進程中開啓一個線程放音樂,也可以開啓另外的線程聊qq,但是進程之間的內存獨立,而屬於同一個進程多個線程之間的內存是共享的,多個線程可以直接對它們所在進程的內存數據進行讀寫並在線程間進行交換。

進程與線程之間的關係

先推薦一個鏈接,這篇文章用漫畫的形式講解了進程與線程的關係:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
這裏寫圖片描述
這裏寫圖片描述
  如上圖,假裝我們已經看完了上面的連接。這裏來爲偷懶的同志們解釋一下,左圖爲進程與線程之間的關係。每個進程都有屬於自己的線程,至少一個。右圖是進程、單線程進程,多線程進程在內存中的情況。

關於python線程的那個傳說:

在python界一直有着一個古老的傳說,那就是python的多線程是雞肋,那麼這個傳說的信度到底有多少呢?如果我們的代碼是CPU密集型(涉及到大量的計算),多個線程的代碼很有可能是線性執行的,所以這種情況下多線程是雞肋,效率可能還不如單線程,因爲有context switch(其實就是線程之間的切換和線程的創建等等都是需要消耗時間的);但是:如果是IO密集型,多線程可以明顯提高效率。例如製作爬蟲,絕大多數時間爬蟲是在等待socket返回數據。這個時候C代碼裏是有release GIL的,最終結果是某個線程等待IO的時候其他線程可以繼續執行。
  那麼,爲什麼我們大python會這麼不智能呢?我們都知道,python是一種解釋性語言,在python執行的過程中,需要解釋器一邊解釋一邊執行,我們之前也介紹了,同一個進程的線程之間內存共享,那麼就會出現內存資源的安全問題,python爲了線程安全,就設置了全局解釋器鎖機制,既一個進程中同時只能有一個線程訪問cpu。作爲解釋型語言,python能引入多線程的概念就已經非常不易了,目前看到的資料php和perl等多線程機制都是不健全的。解釋型語言做多線程的艱難程度可以想見。。。具體下面的鏈接推薦:python的最難問題。
  正是由於python多線程的缺陷,我們在這裏需要引入協成的概念。
  
什麼是協程?
  協程是一種用戶態的輕量級線程。如果說多進程對於多CPU,多線程對應多核CPU,那麼事件驅動和協程則是在充分挖掘不斷提高性能的單核CPU的潛力。我們既可以利用異步優勢,又可以避免反覆系統調用,還有進程切換造成的開銷,這就是協程。協程也是單線程,但是它能讓原來要使用異步+回調方式寫的非人類代碼,可以用看似同步的方式寫出來。它是實現推拉互動的所謂非搶佔式協作的關鍵。對於python來說,由於python多線程中全局解釋器導致的同時只能有一個線程訪問cpu,所以對協程需求就相比於其他語言更爲緊迫。
進程、線程與協程
  從硬件發展來看,從最初的單核單CPU,到單核多CPU,多核多CPU,似乎已經到了極限了,但是單核CPU性能卻還在不斷提升。server端也在不斷的發展變化。如果將程序分爲IO密集型應用和CPU密集型應用,二者的server的發展如下:
    IO密集型應用: 多進程->多線程->事件驅動->協程
    CPU密集型應用:多進程–>多線程程

python——進程基礎

 我們現在都知道python的多線程是個坑了,那麼多進程在這個時候就變得很必要了。多進程實現了多CPU的利用,效率簡直棒棒噠~~~

擁有一個多進程程序:
這裏寫圖片描述
multiprocess Code 1
 按照上面的方法,我們就在自己的代碼中啓動了一個子進程,需要注意的是要想啓動一個子進程,必須加上那句if name == “main”,否則就會報錯。 查看了官方文檔說:Safe importing of main module,Make sure that the main module can be safely imported by a new Python interpreter without causing unintended side effects (such a starting a new process).大概就是說,如果我們必須確定當前已經引入了主模塊,來避免一些非預期的副作用。。。總之,加上!就對了!!!

進程池:
這裏寫圖片描述
multiprocessing Pool Code
 上圖中的方法就是進程池的使用,這裏重點的介紹一些進程池相關的方法。
 首先,我們爲進程注入func,有兩種方式:apply_async表示異步,就是子進程接收到請求之後就各自去執行了,而apply表示同步,子進程們將一個一個的執行,後一個子進程的執行永遠以前一個子進程的結束爲信號,開始執行。還是喫飯的例子。。。異步就是當我通知子進程要去喫飯的時候,他們就同時去喫飯了,同步就是他們必須一個一個的去,前一個沒回來,後一個就不能去。
 close方法:說關閉進程池,至此,進程池中不在有進程可以接受任務。
 terminate和join是一對方法,表示的內容截然相反,執行terminate是結束當前進程池中的所有進程,不管值沒執行完。join方法是阻塞主進程,等待子進程執行完畢,再繼續執行主進程。需要注意的是:這兩個方法都必須在close方法之後執行。當然我們也可以不執行這兩個方法,那麼子進程和主進程就各自執行各自的,無論執行到哪裏,子進程會隨着主進程的結束而結束。。。
獲取進程池中進程的執行結果
這裏寫圖片描述
multiprocessing get result example Code

進程之間的內存共享:
 我們之前說過,正常情況下,每個進程都擁有自己的內存空間,因此進程間的內存是無法共享的。
 但是python卻提供了我們方法,讓我們程序的子進程之間實現簡單的數據共享。
 一個是Array數組,一個是multiprocessing模塊中的Manager類。需要注意的是,Array數組的大小必須固定,Manager需要在linux系統下運行。

python——線程與多線程基礎

  我們之前已經初步瞭解了進程、線程與協程的概念,現在就來看看python的線程。下面說的都是一個進程裏的故事了,暫時忘記進程和協程,先來看一個進程中的線程和多線程。這篇博客將要講一些單線程與多線程的基礎,它們在執行中對cpu資源的分配,幫助還不瞭解多線程的小夥伴一招get寫多線程代碼的技能。已經瞭解的請自行跳過。
  
單線程

這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述

 從上面的圖中我們可以看出,這段代碼執行了10秒多,這就是一段單單線程的一條道走到黑的代碼,它們順序執行,該sleep的時候就sleep,該print的時候就print。右邊的圖是python執行的時候所佔用的cpu的情況。

多線程

 但是,我們是無法忍受一共打印10個數,每個數之間還要sleep這個事實的,所以又出現了多線程,當一個線程sleeping的時候,cpu就去執行其他線程的內容了。例如:

這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述

 看上面的圖,我們引入了threading模塊,並使用Thread類實現了一個多線程的程序,這時,我們僅僅用了9毫秒的時間,就執行完了10個數字的打印。是因爲我們將print這件事情,放到了多個線程中去執行,那麼這幾個線程就幾乎同步去做事,表面上線程都在執行完打印之後進入了休眠狀態,但是一個線程休息的間隙,cpu就可以去完成其他線程的任務了。看最右側的時間圖,我放大了時間軸,其實每一個顏色塊就代表了他們在cpu中執行時佔用的時間,它們之間的差別很小,大概是秒的-3次方這個數量級,足矣被我們忽略了,所以我們感覺他們是同時執行的,當線程執行sleep的時候,他們也幾乎會同時開始計時,同時結束。我們看中間的結果圖,打印的並不像上面單線程那麼漂亮,這也是各個線程搶佔輸出資源的結果。於是我們知道了,多線程的執行幾乎是同步的,並且共享內存,但是它會產生資源搶佔的情況。

get一段多線程代碼
這裏寫圖片描述
 看上面的圖,就是這樣,其實開啓一個線程非常簡單,只需要引入一個threading包,然後初始化一個Thread的對象,將方法名和其參數作爲Thread類初始化的參數傳進去,再使用Thread的對象調用start方法,我們就啓動了一個新的線程。我們可以在自己的程序中按照需求起一個或很多個線程。就像上面那樣。
 threading Code

python——線程與多線程進階

 之前我們已經學會如何在代碼塊中創建新的線程去執行我們要同步執行的多個任務,但是線程的世界遠不止如此。接下來,我們要介紹的是整個threading模塊。threading基於Java的線程模型設計。鎖(Lock)和條件變量(Condition)在Java中是對象的基本行爲(每一個對象都自帶了鎖和條件變量),而在Python中則是獨立的對象,所以python的threading模塊中還提供了Lock,Rlock,Condition,Event等常用類,它們在python中是獨立於Tread模塊的,但是卻與線程緊密相關,不可分割。
 需要注意的是:python的線程中沒有優先級、線程組,也不能被停止、暫停、恢復、中斷,線程只能隨着線程中的代碼執行完畢而被銷燬。查了n多資料之後終於接受了以上事實,個人覺得這是python的一個坑,導致了我在實現線程池的時候無法停止已經注入了方法且執行超時的線程。

threading模塊提供的類:
  Thread, Lock, Rlock, Condition, [Bounded]Semaphore, Event, Timer, local.
  
threading 模塊提供的常用方法:
  threading.currentThread(): 返回當前的線程變量。
  threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啓動後、結束前,不包括啓動前和終止後的線程。
  threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。
  
threading模塊常用類詳解
Thread類:我們使用Thread類來創建新的線程

start 線程準備就緒,等待CPU調度
setName 爲線程設置名稱
getName 獲取線程名稱
setDaemon 設置爲後臺線程或前臺線程(默認)
如果是後臺線程,主線程執行過程中,後臺線程也在進行,主線程執行完畢後,後臺線程不論成功與否,均停止
如果是前臺線程,主線程執行過程中,前臺線程也在進行,主線程執行完畢後,等待前臺線程也執行完成後,程序停止
join 逐個執行每個線程,執行完畢後繼續往下執行,該方法是有高級用法的,代碼在下面
run 線程被cpu調度後執行Thread類對象的run方法

join進階用法
Lock類和Rlock類:由於線程之間隨機調度:某線程可能在執行n條後,CPU接着執行其他線程。爲了多個線程同時操作一個內存中的資源時不產生混亂,我們使用鎖

acquire   給線程上鎖
release   給線程解鎖
這裏寫圖片描述
這裏寫圖片描述

 無論是lock還是rlock,提供的方法都非常簡單,acquire和release。但是rlock和lock的區別是什麼呢?RLock允許在同一線程中被多次acquire。而Lock卻不允許這種情況。注意:如果使用RLock,那麼acquire和release必須成對出現,即調用了n次acquire,必須調用n次的release才能真正釋放所佔用的鎖。

lock vs rlock Code
Condition類:條件變量對象能讓一個線程停下來,等待其它線程滿足了某個“條件”。如,狀態的改變或值的改變。

  • acquire   給線程上鎖
  • wait wait方法釋放當前線程佔用的鎖,同時掛起線程,直至被喚醒或超時(需timeout參數)。當線程被喚醒並重新佔有鎖的時候,程序纔會繼續執行下去。
  • notify   喚醒一個掛起的線程(如果存在掛起的線程)。注:notify()方法不會釋放所佔用的鎖。
  • notifyall  調用這個方法將通知等待池中所有線程,這些線程都將進入鎖定池嘗試獲得鎖定。此方法不會釋放鎖定。使用前線程必須已獲得鎖定,否則將拋出異常。

 比較經典的例子是下面這個生產者與消費者的例子,這個例子網上一搜到處都是,這裏簡單解釋一下這段代碼的意義,代碼中寫了兩個類,Consumer和Producer,分別繼承了Thread類,我們分別初始化這兩個類獲得了c和p對象,並啓動這兩個線程。則這兩個線程去執行run方法(這裏與Thread類內部的調度有關),定義了producer全局變量和condition對象爲全局變量,當producer不大於1時,消費者線程被condition對象阻塞,不能繼續消費(這裏是不再遞減),當producer不小於10時,生產者線程被condition對象阻塞,不再生產(這裏是不再累加),代碼在下面,拿去執行,斷點一下就明白了。

condition Code
Event類:通用的條件變量。多個線程可以等待某個事件的發生,在事件發生後,所有的線程都會被激活。

event.wait(timeout)  當Flag爲‘False’時,線程將被阻塞
clear     將“Flag”設置爲False
set     將“Flag”設置爲True
is_set 返回當前‘Flag’
  

這是一個比較關鍵的類,我在寫線程池的時候看到python的threadpool模塊也用到了。它的意義在於可以控制屬於同一個線程類的多個實例化對象,讓他們同時阻塞或者執行。配合隊列來實現一個線程池非常好用。在接下里的博客中我們還要繼續介紹。先放一個小例子在這裏練練手,瞭解一下event的用法。

event Code
參考文獻:

python多線程學習小結:http://www.myexception.cn/perl-python/1688021.html
python線程指南:http://www.cnblogs.com/huxi/archive/2010/06/26/1765808.html
threading.RLock和threading.Lock:http://blog.sina.com.cn/s/blog_5dd2af0901012rad.html
python的進程、線程與協程:http://www.cnblogs.com/wupeiqi/articles/5040827.html

python——有一種線程池叫做自己寫的線程池

 python的線程一直被稱爲雞肋,所以它也沒有親生的線程池,但是竟然被我發現了野生的線程池,簡直不能更幸運。於是,我開始啃源碼,實在是虐心,在啃源碼的過程中,我簡略的瞭解了python線程的相關知識,感覺還是很有趣的,於是寫博客困難症患者一夜之間化身寫作小能手,完成了一系列線程相關的博客,然後恍然發現,python的多線程是一個雞肋哎。。。這裏換來了同事們的白眼若干→_→。嘻嘻,但是雞肋歸雞肋,看懂了一篇源碼給我帶來的收穫和成就感還是不能小視,所以還是分享下。
  
別人的線程池
 首先介紹別人寫的線程池模塊,野生threadpool,直接到pypi上去搜,或者pip安裝,都可以get到。這裏還是先貼上來:
 threadpool Code
這裏寫圖片描述

 首先我們來看這個線程池的大致原理。在初始化中,它會根據我們的需求,啓動相應數量的線程,這些線程是初始化好的,一直到程序結束,不會停止,它們從任務隊列中獲取任務,在沒有任務的時候就阻塞,他們當我們有任務的時候,對任務進行初始化,放入任務隊列,拿到任務的線程結束了自己的阻塞人生,歡歡喜喜的拿回去執行,並在執行完畢之後,將結果放入結果隊列,繼續到任務隊列中取任務,如果沒有任務就進入阻塞狀態。看了一整天的源碼竟讓我三兩句話解釋清楚了,我到底是表達能力強還是理解能力差!!!我想靜靜~~~附上類圖如下:
這裏寫圖片描述

我的線程池

 下面就來介紹我寫的線程池了,上面的線程池有一個問題,那就是一開始創建了多少個線程,這些線程就一直存在內存中,即使沒有工作,也不會銷燬。於是我有了一個想法,就像其他語言中的線程池一樣,寫一個擁有最大線程數和最小線程數限制的線程池。
這裏寫圖片描述
 程序啓動之初只將最小線程數的線程放在池中,並將線程設置爲阻塞狀態,用守護線程來查看任務隊列,當任務隊列中有任務時,則停止線程的阻塞狀態,讓它們到隊列中去獲取任務,執行,如果需要返回結果,則將結果返回結果隊列。當任務很多,線程池中沒有閒置的線程且當前線程數小於線程池最大線程數時,將創建新的線程(這裏使用了yield)來接收新的任務,線程執行完畢後,則回到阻塞狀態,長期閒置的線程會自動銷燬,但池中線程永遠不小於在最小線程數。當最小線程數和最大線程數相等的時候,內部就基本和野生線程相同啦
 在參考了野生threadpool模塊之後,我也學着繼承原生的threading.Thread類,並重寫了run方法,瞭解了給一個線程注入新方法的過程。並用到了Event方法和yield。如果不要返回值的話,我想效率還是很高的。儘管我在返回值方面還做了優化

銀角大王的線程池:

threadpool Code
use example Code
  這裏安利下我男神,哈哈哈~武sir的方法和上面的例子中不同的是,自定義了線程的start方法,當啓動線程的時候才初始化線程池,並根據線程池定義的數量和任務數量取min,而不是先開啓定義的線程數等待命令,在一定程度上避免了空線程對內存的消耗。
with知識點
  這裏要介紹一個知識點。我們在做上下文管理的時候,用到過with。
  我們如何自定義一個with方法呢?
這裏寫圖片描述
  如此一來,我們便可以實現對線程狀態的監控和管理了。將正在運行中的線程,加入到一個列表中,並使用yield返回,當線程執行完之後,再從這個列表中移除,就可以知道哪些線程是正在運行的啦。

python——協程

  由於python中的多線程比較特殊,所以協程的概念就變得尤爲珍貴了,對於cpu密集型的操作,使用協程的效率無疑要好過多線程很多。因爲協程的創建及其間切換的時間成本要低於線程很多。也因爲這一點,很多人說,協程纔是python的未來,重要不重要!!!
  python中提供協程的模塊有兩個,greenlet和gevent。greenlet和gevent最大的區別在於greenlet需要你自己來處理線程切換, 就是說,你需要自己指定現在執行哪個greenlet再執行哪個greenlet。ps:這兩個包都不是python自帶的,所以需要手動安裝一下,pip就可以輕鬆搞定!
這裏寫圖片描述
這裏寫圖片描述

  左側圖是greenlet的用法,我已經將執行順序標註出來了,從圖中我們不難看出greenlet的執行順序是需要我們手動控制的,現在再看看右側的圖是gevent的用法,就智能多了,它不需要我們自己去支配,只要一個協程稍有空閒,gevent就幫你進行切換,已達到cpu的最大利用率。
greenlet Code
gevent example1 Code
  這裏再贈送一個gevent遇到IO操作自動切換的例子,這段代碼一看就是一副高大上的樣子,從老師那裏偷來的,嘻:
gevent example2 Code

參考文獻:
    python的線程進程和協成:http://www.cnblogs.com/wupeiqi/articles/5040827.html
    greenlet背景介紹與實現機制:http://blog.jobbole.com/77240/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章