多任務:進程、線程、協程總結及關係

多線程:python


1. 對線程的理解緩存

1.一個程序運行起來至少有一個進程,一個進程至少有一個線程
2.處理器cpu分配給線程,即cpu真正運行的是線程中的代碼
3.分配cpu給線程時,是經過時間片輪訓方式進行的
4.進程是操做系統分配程序執行資源的單位,而線程是進程的一個實體,
 是CPU調度和分配的單位。安全

 

2. python實現多線程的兩種方式
python的thread模塊是比較底層的模塊,python的threading模塊是對thread作了一些包裝的,能夠更加方便的被使用,經過threading模塊能夠建立線程,通常咱們都使用threading網絡

 

3. 線程什麼時候開啓,什麼時候結束
1.子線程什麼時候開啓,什麼時候運行
   當調用thread.start()時 開啓線程,再運行線程的代碼
2.子線程什麼時候結束
   子線程把target指向的函數中的語句執行完畢後,或者線程中的run函數代碼執行完畢後,當即結束當前子線程
3.查看當前線程數量
   經過threading.enumerate()可枚舉當前運行的全部線程
4.主線程什麼時候結束
   全部子線程執行完畢後,主線程才結束多線程

 

4.多線程的建立與執行都是無序的,同一個進程裏面的多線程共享全局變量,全部對於多個線程間共享數據很方便,執行效率也就比多進程更高;但缺點就是容易形成多線程對全局變量的隨意遂改,就可能致使全局變量的混亂(即線程是非安全的),還有若是多個線程同時對一個全局變量操做,還會出現資源競爭問題,從而致使數據結果不正確,即會遇到線程安全問題。對於線程的安全問題,咱們會使用同步機制解決,同步就是協同步調,按預約的前後次序進行運行(這裏的同步實質上是咱們生活上的異步)。咱們最經常使用的同步機制就是使用互斥鎖,互斥鎖爲資源引入一個狀態:鎖定/非鎖定;某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲「鎖定」,其餘線程不能更改;直到該線程釋放資源,將資源的狀態變成「非鎖定」,其餘的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操做,從而保證了多線程狀況下數據的正確性。併發

 

上鎖解鎖過程:
當一個線程調用鎖的acquire()方法得到鎖時,鎖就進入「locked」狀態。
每次只有一個線程能夠得到鎖。若是此時另外一個線程試圖得到這個鎖,該線程就會變爲「blocked」狀態,稱爲「阻塞」,直到擁有鎖的線程調用鎖的release()方法釋放鎖以後,鎖進入「unlocked」狀態。
線程調度程序從處於同步阻塞狀態的線程中選擇一個來得到鎖,並使得該線程進入運行(running)狀態。app

 

多進程:dom


程序:好比電腦安裝了不少程序,又好比咱們編寫一個xxx.py程序,它們靜靜的保存在硬盤中,因此程序是一個靜態的概念
進程:一個程序運行起來後,代碼+用到的資源 稱之爲進程,它是操做系統分配資源的基本單位。異步

 1.進程的狀態
工做中,任務數每每大於cpu的核數,即必定有一些任務正在執行,而另一些任務在等待cpu進行執行,所以致使了有了不一樣的狀態async

 • 就緒態:運行的條件都已經知足,正在等在cpu執行
 • 執行態:cpu正在執行其功能
 • 等待態:等待某些條件知足,例如一個程序sleep了,此時就處於等待態,紅綠燈,等待消息回覆,等待同步鎖 等都是處於等待態

2.進程的建立-multiprocessing
multiprocessing模塊就是跨平臺版本的多進程模塊,提供了一個Process類來建立進程對象,進程之間不共享全局變量

3. Process語法結構以下:
Process([group [, target [, name [, args [, kwargs]]]]])
• target:若是傳遞了函數的引用,能夠認爲這個子進程就執行這裏的代碼
• args:給target指定的函數傳遞的參數,以元組的方式傳遞
• kwargs:給target指定的函數傳遞命名參數
• name:給進程設定一個名字,能夠不設定
• group:指定進程組,大多數狀況下用不到
Process建立的實例對象的經常使用方法:
• start():啓動子進程實例(建立子進程)
• is_alive():判斷進程子進程是否還在活着
• join([timeout]):是否等待子進程執行結束,或等待多少秒
• terminate():無論任務是否完成,當即終止子進程
Process建立的實例對象的經常使用屬性:
• name:當前進程的別名,默認爲Process-N,N爲從1開始遞增的整數
• pid:當前進程的pid(進程號)

 

進程、線程對比:


功能
• 進程,可以完成多任務,好比運行的QQ再單獨開一個進程接收推送的消息
• 線程,可以完成多任務,好比運行的QQ開多個線程來發送消息、接收文件、視頻聊天等多個任務
定義的不一樣
• 進程是操做系統進行資源分配和調度的一個基本單位.
• 線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程本身基本上不擁有系統資源,可是它可與同屬一個進程的其餘的線程共享進程所擁有的所有資源.


區別
• 一個程序至少有一個進程,一個進程至少有一個線程.
• 線程的劃分尺度小於進程(資源比進程少),使得多線程程序的併發性高。
• 進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率
• 線程不可以獨立執行,必須依存在進程中
• 能夠將進程理解爲工廠中的一條流水線,而其中的線程就是這個流水線上的工人

多進程適合在 CPU 密集型操做(cpu 操做指令比較多,如位數多的浮點運算)。
多線程適合在 IO 密集型操做(讀寫數據操做較多的,好比爬蟲)

 

優缺點
線程和進程在使用上各有優缺點:線程執行開銷小,但不利於資源的管理和保護;而進程正相反。

 

進程間通訊-Queue
Process之間有時須要通訊,操做系統提供了不少機制來實現進程間的通訊。


1. Queue的使用
可使用multiprocessing模塊的Queue實現多進程之間的數據傳遞,Queue自己是一個消息列隊程序,首先用一個小實例來演示一下Queue的工做原理:

#coding=utf-8
from multiprocessing import Queue q=Queue(3) #初始化一個Queue對象,最多可接收三條put消息
q.put("消息1") q.put("消息2") print(q.full())  #False
q.put("消息3") print(q.full()) #True

#由於消息列隊已滿下面的try都會拋出異常,第一個try會等待2秒後再拋出異常,第二個Try會馬上拋出異常
try: q.put("消息4",True,2) except: print("消息列隊已滿,現有消息數量:%s"%q.qsize()) try: q.put_nowait("消息4") except: print("消息列隊已滿,現有消息數量:%s"%q.qsize()) #推薦的方式,先判斷消息列隊是否已滿,再寫入
if not q.full(): q.put_nowait("消息4") #讀取消息時,先判斷消息列隊是否爲空,再讀取
if not q.empty(): for i in range(q.qsize()): print(q.get_nowait())

"""

運行結果:

False True 消息列隊已滿,現有消息數量:3 消息列隊已滿,現有消息數量:3 消息1 消息2 消息3
"""

說明

初始化Queue()對象時(例如:q=Queue()),若括號中沒有指定最大可接收的消息數量,或數量爲負值,那麼就表明可接受的消息數量沒有上限(直到內存的盡頭);

  • Queue.qsize():返回當前隊列包含的消息數量;

  • Queue.empty():若是隊列爲空,返回True,反之False ;

  • Queue.full():若是隊列滿了,返回True,反之False;

  • Queue.get([block[, timeout]]):獲取隊列中的一條消息,而後將其從列隊中移除,block默認值爲True;

    1)若是block使用默認值,且沒有設置timeout(單位秒),消息列隊若是爲空,此時程序將被阻塞(停在讀取狀態),直到從消息列隊讀到消息爲止,若是設置了timeout,則會等待timeout秒,若還沒讀取到任何消息,則拋出"Queue.Empty"異常;

    2)若是block值爲False,消息列隊若是爲空,則會馬上拋出"Queue.Empty"異常;

  • Queue.get_nowait():至關Queue.get(False);

  • Queue.put(item,[block[, timeout]]):將item消息寫入隊列,block默認值爲True;

    1)若是block使用默認值,且沒有設置timeout(單位秒),消息列隊若是已經沒有空間可寫入,此時程序將被阻塞(停在寫入狀態),直到從消息列隊騰出空間爲止,若是設置了timeout,則會等待timeout秒,若還沒空間,則拋出"Queue.Full"異常;

    2)若是block值爲False,消息列隊若是沒有空間可寫入,則會馬上拋出"Queue.Full"異常;

  • Queue.put_nowait(item):至關Queue.put(item, False);

 

進程池Pool

當須要建立的子進程數量很少時,能夠直接利用multiprocessing中的Process動態成生多個進程,但若是是上百甚至上千個目標,手動的去建立進程的工做量巨大,此時就能夠用到multiprocessing模塊提供的Pool方法。

初始化Pool時,能夠指定一個最大進程數,當有新的請求提交到Pool中時,若是池尚未滿,那麼就會建立一個新的進程用來執行該請求;但若是池中的進程數已經達到指定的最大值,那麼該請求就會等待,直到池中有進程結束,纔會用以前的進程來執行新的任務,請看下面的實例:

# -*- coding:utf-8 -*-
from multiprocessing import Pool import os, time, random def worker(msg): t_start = time.time() print("%s開始執行,進程號爲%d" % (msg,os.getpid())) # random.random()隨機生成0~1之間的浮點數
    time.sleep(random.random()*2) t_stop = time.time() print(msg,"執行完畢,耗時%0.2f" % (t_stop-t_start)) po = Pool(3)  # 定義一個進程池,最大進程數3
for i in range(0,10): # Pool().apply_async(要調用的目標,(傳遞給目標的參數元祖,))
    # 每次循環將會用空閒出來的子進程去調用目標
 po.apply_async(worker,(i,)) print("----start----") po.close() # 關閉進程池,關閉後po再也不接收新的請求
po.join()  # 等待po中全部子進程執行完成,必須放在close語句以後
print("-----end-----")

""" 運行結果:
----start---- 0開始執行,進程號爲21466 1開始執行,進程號爲21468 2開始執行,進程號爲21467 0 執行完畢,耗時1.01 3開始執行,進程號爲21466 2 執行完畢,耗時1.24 4開始執行,進程號爲21467 3 執行完畢,耗時0.56 5開始執行,進程號爲21466 1 執行完畢,耗時1.68 6開始執行,進程號爲21468 4 執行完畢,耗時0.67 7開始執行,進程號爲21467 5 執行完畢,耗時0.83 8開始執行,進程號爲21466 6 執行完畢,耗時0.75 9開始執行,進程號爲21468 7 執行完畢,耗時1.03 8 執行完畢,耗時1.05 9 執行完畢,耗時1.69 -----end-----
"""

multiprocessing.Pool經常使用函數解析:

  • apply_async(func[, args[, kwds]]) :使用非阻塞方式調用func(並行執行,堵塞方式必須等待上一個進程退出才能執行下一個進程),args爲傳遞給func的參數列表,kwds爲傳遞給func的關鍵字參數列表;
  • close():關閉Pool,使其再也不接受新的任務;
  • terminate():無論任務是否完成,當即終止;
  • join():主進程阻塞,等待子進程的退出, 必須在close或terminate以後使用;

 

進程池中的Queue

若是要使用Pool建立進程,就須要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),不然會獲得一條以下的錯誤信息:

RuntimeError: Queue objects should only be shared between processes through inheritance.

下面的實例演示了進程池中的進程如何通訊:

# -*- coding:utf-8 -*-

# 修改import中的Queue爲Manager
from multiprocessing import Manager,Pool import os,time,random def reader(q): print("reader啓動(%s),父進程爲(%s)" % (os.getpid(), os.getppid())) for i in range(q.qsize()): print("reader從Queue獲取到消息:%s" % q.get(True)) def writer(q): print("writer啓動(%s),父進程爲(%s)" % (os.getpid(), os.getppid())) for i in "itcast": q.put(i) if __name__=="__main__": print("(%s) start" % os.getpid()) q = Manager().Queue()  # 使用Manager中的Queue
    po = Pool() po.apply_async(writer, (q,)) time.sleep(1)  # 先讓上面的任務向Queue存入數據,而後再讓下面的任務開始從中取數據
 po.apply_async(reader, (q,)) po.close() po.join() print("(%s) End" % os.getpid()) """ 運行結果: writer啓動(11097),父進程爲(11095) reader啓動(11098),父進程爲(11095) reader從Queue獲取到消息:i reader從Queue獲取到消息:t reader從Queue獲取到消息:c reader從Queue獲取到消息:a reader從Queue獲取到消息:s reader從Queue獲取到消息:t """

 

協程:
協程,又稱微線程,纖程。英文名Coroutine。

 

協程是啥:
協程是python箇中另一種實現多任務的方式,只不過比線程更小佔用更小執行單元(理解爲須要的資源)。
通俗的理解:在一個線程中的某個函數,能夠在任何地方保存當前函數的一些臨時變量等信息,而後切換到另一個函數中執行,注意不是經過調用函數的方式作到的,而且切換的次數以及何時再切換到原來的函數都由開發者本身肯定

協程和線程差別:
在實現多任務時, 線程切換從系統層面遠不止保存和恢復 CPU上下文這麼簡單。 操做系統爲了程序運行的高效性每一個線程都有本身緩存Cache等等數據,操做系統還會幫你作這些數據的恢復操做。 因此線程的切換很是耗性能。可是協程的切換隻是單純的操做CPU的上下文,因此一秒鐘切換個上百萬次系統都抗的住。

 

gevent
greenlet已經實現了協程,可是這個還的人工切換,是否是以爲太麻煩了,不要捉急,python還有一個比greenlet更強大的而且可以自動切換任務的模塊gevent
其原理是當一個greenlet遇到IO(指的是input output 輸入輸出,好比網絡、文件操做等)操做時,好比訪問網絡,就自動切換到其餘的greenlet,等到IO操做完成,再在適當的時候切換回來繼續執行。
因爲IO操做很是耗時,常常使程序處於等待狀態,有了gevent爲咱們自動切換協程,就保證總有greenlet在運行,而不是等待IO


進程、線程、協程對比
請仔細理解以下的通俗描述
• 有一個老闆想要開個工廠進行生產某件商品(例如剪子)
• 他須要花一些財力物力製做一條生產線,這個生產線上有不少的器件以及材料這些全部的 爲了可以生產剪子而準備的資源稱之爲:進程
• 只有生產線是不可以進行生產的,因此老闆的找個工人來進行生產,這個工人可以利用這些材料最終一步步的將剪子作出來,這個來作事情的工人稱之爲:線程
• 這個老闆爲了提升生產率,想到3種辦法:
1. 在這條生產線上多招些工人,一塊兒來作剪子,這樣效率是成倍増長,即單進程 多線程方式
2. 老闆發現這條生產線上的工人不是越多越好,由於一條生產線的資源以及材料畢竟有限,因此老闆又花了些財力物力購置了另一條生產線,而後再招些工人這樣效率又再一步提升了,即多進程 多線程方式
3. 老闆發現,如今已經有了不少條生產線,而且每條生產線上已經有不少工人了(即程序是多進程的,每一個進程中又有多個線程),爲了再次提升效率,老闆想了個損招,規定:若是某個員工在上班時臨時沒事或者再等待某些條件(好比等待另外一個工人生產完謀道工序 以後他才能再次工做) ,那麼這個員工就利用這個時間去作其它的事情,那麼也就是說:若是一個線程等待某些條件,能夠充分利用這個時間去作其它事情,其實這就是:協程方式


簡單總結
 1. 進程是操做系統資源分配的單位
 2. 線程是CPU調度的單位
 3. 進程切換須要的資源最大,效率很低
 4. 線程切換須要的資源通常,效率通常(固然在不考慮GIL的狀況下)
 5. 協程切換任務資源很小,效率高
 6. 多進程、多線程根據cpu核數不同多是並行的,可是協程是在一個線程中 因此是併發

相關文章
相關標籤/搜索