標籤:需要 方式 唯一性 通訊 情況 %s 進程與線程的區別 非同步io 多線程
什麼是進程/線程
眾所周知,CPU是電腦的核心,它承擔了所有的計算任務。而作業系統是電腦的管理者,是一個大管家,它負責任務的調度,資源的分配和管理,統領整個電腦硬體。應用程式是具有某種功能的程式,程式運行與作業系統之上。
進程
進程時一個具有一定功能的程式在一個資料集上的一次動態執行過程。進程由程式,資料集合和進程式控制制塊三部分組成。程式用於描述進程要完成的功能,是控制進程執行的指令集;資料集合是程式在執行時需要的資料和工作區;程式控制塊(PCB)包含程式的描述資訊和控制資訊,是進程存在的唯一標誌。
線程
在很早的時候電腦並沒有線程這個概念,但是隨著時代的發展,只用進程來處理常式出現很多的不足。如當一個進程堵塞時,整個程式會停止在堵塞處,並且如果頻繁的切換進程,會浪費系統資源。所以線程出現了。
線程是能擁有資源和獨立啟動並執行最小單位,也是程式執行的最小單位。一個進程可以擁有多個線程,而且屬於同一個進程的多個線程間會共用該進行的資源。
進程與線程的區別
- 一個進程由一個或者多個線程組成,線程是一個進程中代碼的不同執行路線。
- 切換進程需要的資源比切換線程的要多的多。
- 進程之間相互獨立,而同一個進程下的線程共用程式的記憶體空間(如程式碼片段,資料集,堆棧等)。某進程內的線程在其他進程不可見。換言之,線程共用同一片記憶體空間,而進程各有獨立的記憶體空間。
以下是作者在知乎上看到的關於進程與線程的討論,其中一個我感覺很有道理,摘抄如下:
zhonyong
連結:https://www.zhihu.com/question/25532384/answer/81152571
首先來一句概括的總論:進程和線程都是一個時間段的描述,是CPU工作時間段的描述。下面細說背景:CPU+RAM+各種資源(比如顯卡,光碟機,鍵盤,GPS, 等等外設)構成我們的電腦,但是電腦的運行,實際就是CPU和相關寄存器以及RAM之間的事情。一個最最基礎的事實:CPU太快,太快,太快了,寄存器僅僅能夠追的上他的腳步,RAM和別的掛在各匯流排上的裝置完全是望其項背。那當多個任務要執行的時候怎麼辦呢?輪流著來?或者誰優先順序高誰來?不管怎麼樣的策略,一句話就是在CPU看來就是輪流著來。一個必須知道的事實:執行一段程式碼,實現一個功能的過程介紹 ,當得到CPU的時候,相關的資源必須也已經就位,就是顯卡啊,GPS啊什麼的必須就位,然後CPU開始執行。這裡除了CPU以外所有的就構成了這個程式的執行環境,也就是我們所定義的程式上下文。當這個程式執行完了,或者分配給他的CPU執行時間用完了,那它就要被切換出去,等待下一次CPU的臨幸。在被切換出去的最後一步工作就是儲存程式上下文,因為這個是下次他被CPU臨幸的運行環境,必須儲存。串聯起來的事實:前面講過在CPU看來所有的任務都是一個一個的輪流執行的,具體的輪流方法就是:先載入程式A的上下文,然後開始執行A,儲存程式A的上下文,調入下一個要執行的程式B的程式上下文,然後開始執行B,儲存程式B的上下文。。。。
========= 重要的東西出現了========
進程和線程就是這樣的背景出來的,兩個名詞不過是對應的CPU時間段的描述,名詞就是這樣的功能。進程就是包換環境切換的程式執行時間總和 = CPU載入上下文+CPU執行+CPU儲存上下文線程是什麼呢?進程的顆粒度太大,每次都要有上下的調入,儲存,調出。如果我們把進程比喻為一個運行在電腦上的軟體,那麼一個軟體的執行不可能是一條邏輯執行的,必定有多個分支和多個程式段,就好比要實現程式A,實際分成 a,b,c等多個塊組合而成。那麼這裡具體的執行就可能變成:程式A得到CPU =》CPU載入上下文,開始執行程式A的a小段,然後執行A的b小段,然後再執行A的c小段,最後CPU儲存A的上下文。這裡a,b,c的執行是共用了A的上下文,CPU在執行的時候沒有進行環境切換的。這裡的a,b,c就是線程,也就是說線程是共用了進程的上下文環境,的更為細小的CPU時間段。到此全文結束,再一個總結:進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同。
開進程需要時間
學習《python爬蟲開發與項目實踐》時,執行下面一段代碼:
from multiprocessing import Processimport osdef run_process(name): print("Child process %s (%s) is running" % (name,os.getpid()))if __name__ == "__main__": print("parant process %s " % os.getpid()) for i in range(5): p = Process(target=run_process, args=(str(i),)) print("process will start") p.start() p.join() print("process end")
顯示的結果是
parant process 6332 process will startprocess will startprocess will startprocess will startprocess will startChild process 2 (9896) is runningChild process 0 (11208) is runningChild process 3 (5464) is runningChild process 1 (10208) is runningChild process 4 (12596) is runningprocess end
可以看到,程式在執行完
print ("parant process %s " % os.getpid())
沒有接著馬上執行run_process(),而是先列印process will start,最後把子進程一起執行。這是因為子進程的建立是需要時間的,在這個空閑時間裡父進程繼續執行代碼,而子進程在建立完成後顯示。
Pool進程池
需要建立多個進程時,可以使用multiprocessing中的Pool類開進程池。Pool()預設開啟數量等於當前cpu核心數的子進程(當然可以手動改變)
from multiprocessing import Pooldef hello(i): print("hello ,this is the %d process" % i)def main(): p = Pool() for i in range(1,5): p.apply_async(target=hell0,args=(i,)) p.close p.joinif __name__ == "__main__": main()
apply_async表示在開進程時不阻塞主進程,是非同步IO的一種方式之一。targe參數傳入要在子線程中執行的函數對象,args以元組的方式傳入函數的參數。
join會等待線程池中的每一個線程執行完畢,在調用join之前必須要先調用close,close表示不能再向線程池中添加新的process了。
進程間的通訊
每個進程各自有不同的使用者地址空間,任何一個進程的全域變數在另一個進程中都看不到,所以進程之間要交換資料必須通過核心,在核心中開闢一塊緩衝區,進程A把資料從使用者空間拷到核心緩衝區,進程B再從核心緩衝區把資料讀走,核心提供的這種機制稱為處理序間通訊。假如建立了多個進程,那麼進程間的通訊是必不可少的。Python提供了多種進程通訊的方式,其中以Queue和Pipe用得最多。下面分別介紹這兩種模式。
Queue
Queue是一種多進程安全的隊列。實現多進程間的通訊有兩種方法:
- get() 用於向隊列中加入資料。有兩個屬性:blocked和timeout。blocked為true時(預設為True)且timeout為正值時,如果當隊列已滿會阻塞timeout時間,在這個時間內如果隊列有空位會加入,如果超過時間仍然沒有空位會拋出Queue.Full異常。
- put() 用於從隊列中擷取一個資料並將其從隊列中刪除。有兩個屬性:blocked和timeout。blocked為true(預設為True)且timeout為正值時,如果當前隊列為空白會阻塞timeout時間,在這個時間內如果隊列有新資料會擷取,如果超過時間仍然沒有新資料會拋出Queue.Empty異常。
from multiprocessing import Process,Queueimport osdef put_data(q,nums): print(‘現在的進程編號為:%s,這是一個put進程‘ % os.getpid()) for num in nums: q.put(num) print(‘%d已經放入隊列中啦!‘ % num)def get_data(q): print(‘現在的進程編號為:%s,這是一個get進程‘ % os.getpid()) while True: print(‘已經從隊列中擷取%s並從中刪除‘ % q.get())if __name__ == ‘__main__‘: q = Queue() p1 = Process(target=put_data,args=(q,[‘1‘,‘2‘,‘3‘],)) p2 = Process(target=put_data,args=(q,[‘4‘,‘5‘,‘6‘],)) p3 = Process(target=get_data,args=(q,)) p1.start() p2.start() p3.start() p1.join() p2.join() # p3是個死迴圈,需要手動結束這個進程 p3.terminate()
我們來看一下運行結果:
現在的進程編號為:10336,這是一個put進程1已經放入隊列中啦!2已經放入隊列中啦!3已經放入隊列中啦!現在的進程編號為:9116,這是一個get進程已經從隊列中擷取1,並從中刪除已經從隊列中擷取2並從中刪除已經從隊列中擷取3並從中刪除現在的進程編號為:2732,這是一個put進程4已經放入隊列中啦!5已經放入隊列中啦!已經從隊列中擷取4,並從中刪除6已經放入隊列中啦!已經從隊列中擷取5並從中刪除已經從隊列中擷取6並從中刪除
Pipe
Pipe與Queue不同之處在於Pipe是用於兩個進程之間的通訊。就像進程位於一根水管的兩端。讓我們看看Pipe官方文檔的描述:
Returns a pair (conn1, conn2) of Connection objects representing the ends of a pipe.
Piep返回conn1和conn2代表水管的兩端。Pipe還有一個參數duplex(adj. 二倍的,雙重的 n. 雙工;佔兩層樓的公寓套房),預設為True。當duplex為True時,開啟雙工模式,此時水管的兩邊都可以進行收發。當duplex為False,那麼conn1隻負責接受資訊,conn2隻負責發送資訊。
conn通過send()和recv()來發送和接受資訊。值得注意的是,如果管道中沒有資訊可接受,recv()會一直阻塞直到管道關閉(任意一端進程接結束則管道關閉)。
from multiprocessing import Process,Pipeimport osdef put_data(p,nums): print(‘現在的進程編號為:%s,這個一個send進程‘ % os.getpid()) for num in nums: p.send(num) print(‘%s已經放入管道中啦!‘ % num)def get_data(p): print(‘現在的進程編號為:%s,這個一個recv進程‘ % os.getpid()) while True: print(‘已經從管道中擷取%s並從中刪除‘ % p.recv())if __name__ == ‘__main__‘: p = Pipe(duplex=False) # 此時Pipe[1]即是Pipe返回的conn2 p1 = Process(target=put_data,args=(p[1],[‘1‘,‘2‘,‘3‘],)) # 此時Pipe[0]即是Pipe返回的conn1 p3 = Process(target=get_data,args=(p[0],)) p1.start() p3.start() p1.join() p3.terminate()
讓我們看一下輸出結果
現在的進程編號為:9868,這個一個recv進程現在的進程編號為:9072,這個一個send進程1已經放入管道中啦!已經從管道中擷取1,並從中刪除2已經放入管道中啦!已經從管道中擷取2並從中刪除3已經放入管道中啦!已經從管道中擷取3並從中刪除
控制線程
我們是沒有辦法完全人為控制線程的,因為線程由系統控制。但是可以用一些方式來影響線程的調用,比如互斥鎖,sleep(阻塞),死結等。
線程的幾種狀態
建立-----就緒------------------運行-----死亡
等待(阻塞)
線程的生命週期由run方法決定,當run方法結束時線程死亡。可以通過繼承Thread,重寫run方法改變Thread的功能,最後還是通過start()方法開線程。
from threading import Threadclass MyThread(Thread): def run(self): print(‘i am sorry‘)if __name__ == ‘__main__‘: t = MyThread() t.start()
通過args參數以一個元組的方式給線程中的函數傳參。
from threading import Threaddef sorry(name): print(‘i am sorry‘,name)if __name__ == ‘__main__‘: t = Thread(target=sorry,args=(‘mike‘)) t.start()
線程鎖
多線程中任務中,可能會發生多個線程同時對一個公用資源(如全域變數)進行操作的情況,這是就會發生混亂。為了避免這種情況,需要引入線程鎖的概念。只有一個線程能處於上鎖狀態,當一個線程上鎖之後,如果有另外一個線程試圖獲得鎖,該線程就會掛起直到擁有鎖的線程將鎖釋放。這樣就保證了同時只有一個線程對公用資源進行訪問或修改。
from threading import Thread,Locknum = 0def puls(): # 獲得一個鎖 lock = Lock() global num # acquire()方法上鎖 lock.acquire() num += 1 print(num) # release()方法解鎖 lock.release()if __name__ == ‘__main__‘: for i in range(5): t = Thread(target=plus) t.start() t.join()
join()方法會阻塞主線程直到子線程全部結束(也就是同步)。
鎖的用處:
1. 確保某段關鍵代碼只能由一個線程從頭到尾執行,保證了資料的唯一性。
鎖的壞處:
1. 阻止了多線程並發執行,效率大大降低。
2. 由於存在多個鎖,不同的線程持有不同的鎖並試圖擷取對方的鎖時,可能造成死結。
守護線程
線程其實並沒有主次的概念,我們一般說的‘主線程’實際上是main函數的線程,而所謂主線程結束子線程也會結束是因為在主線程結束時調用了系統的退出函數。而守護線程是指‘不重要線程’。主線程會等所有‘重要’線程結束後才結束。通常當Client Access Server時會為這次訪問開啟一個守護線程。將setDaemon屬性設為True即可將該線程設為守護線程。
from threading import Threadn = 100def count(x,y): return n=x+yif __name__ == ‘__main__‘: t = Thread(target=count,args=(1,2)) t.setDaemon = True # ...
python學習交流群:125240963
轉載至:Python中的線程與進程
詳細解析Python中的線程與進程的區別