進程 VS 線程

一、進程和線程

1.1 概念

?網上看到了一篇比較好的解釋,從計算機的發展角度來分析進程和線程的。

早期的計算機是沒有線程這麼一說的,進程就是計算運行的基本單位,包含靜態的資源和動態的計算。隨着計算機性能的提升和系統設計的改進,爲了避免進程之間的調度帶來的開銷,同時提升系統的併發性,於是在進程中引入了線程的概念,專門用來負責程序的動態部分。

線程專門用來描述系統的動態計算,是系統調度和運行的基本單位,而現在的進程已經是一種靜態的概念,可以理解爲爲一個或者多個線程提供共享資源的上下文容器,更多強調的是系統的資源,不在強調動態性,這些動態特性已經被轉移到線程身上。

  • 進程

在一定環境下,把靜態的程序代碼運行起來,通過使用不同的資源來完成一定的任務,這些資源可以成爲計算機執行的上下文環境,進程是資源分配的基本單位,強調的是內存資源的管理。

  • 線程

線程是進程的一部分,扮演的角色是在進程已經掌握的資源下怎麼利用CPU去運行代碼和動態計算,這裏牽扯到的是CPU,寄存器和線程的棧。線程是CPU輪流調度的基本單位,強調的是CPU的運行。

總結起來,就是進程是資源分配的基本單位,而線程是CPU在進程內切換的基本單位,線程屬於進程。

1.2 進程和線程的區別

這裏舉個栗子做類比,進程=火車,線程=車廂

  • 線程在進程下運行(單純的車廂無法運行)
  • 一個進程可以包含多個線程(一輛火車包含多節車廂)
  • 不同進程間共享數據很難(一輛火車的乘客很難跑到另外一輛火車)
  • 同一進程下的不同線程數據很容易共享(A車廂的乘客很容易換到B車廂)
  • 進程要比線程消耗更多的資源(火車比單節車廂消耗的資源更多)
  • 進程之間不會相互影響,一個線程掛掉將導致整個進程掛掉(一列火車不會影響另外一輛火車,但是如果一輛火車上的某節車廂出故障了,將影響整列火車的運行)
  • 線程使用的內存地址可以上鎖,即一個線程使用某些內存的使用,其他線程必須等待它結束才能使用(火車上的洗手間一次只能一個乘客使用)
  • 進程使用的內存地址可以限定使用量——信號量(比如火車上的餐廳,最多隻允許多少人進去,如果滿了需要在門口等待)
  • 進程可以拓展到多級,使用多核

二、多進程和多線程

?這裏參考廖雪峯老師的博客,講的特別好。

2.1 概念

多進程和多線程都是多任務處理。要實現多任務,我們通常會設計Master-Worker模式,即Master負責分配任務,Worker負責執行任務,通常情況下,一個Mater、多個Worker

  • 多進程:Master爲主進程,Worker爲其他進程
  • 多線程:Master爲主線程,Worker爲其他線程

多進程的python代碼

import multiprocessing as mp


def fun(a):
    print(a)


if __name__ == '__main__':
    # 這裏有三個任務,手動指定3個進程
    p1 = mp.Process(target = fun, args=(1,))
    p2 = mp.Process(target = fun, args=(2,))
    p3 = mp.Process(target = fun, args=(3,))
    p1.start()  # 啓動進程
    p2.start()
    p3.start()
    p1.join()  # 等待子進程執行完畢
    p2.join()
    p3.join()

輸出結果

1
2
3

多線程的python代碼

import time, threading

# 新線程執行的代碼:
def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name)


if __name__ == '__main__':
	print('thread %s is running...' % threading.current_thread().name)
	t = threading.Thread(target=loop, name='LoopThread')
	t.start()
	t.join()
	print('thread %s ended.' % threading.current_thread().name)

輸出結果

thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

2.2 多進程和多線程的區別

  • 多進程的最大優點是穩定性高,因爲一個子進程崩潰,不會影響主進程和其他子進程
  • 多進程模式的缺點是創建進程的代價大,在Unix/Linux系統下,用fork()調用還行,但是在Windows下創建進程開銷巨大,另外,操作系統同時運行的進程數有限,在內存和CPU有限的情況下,如果有幾千個進程同時運行,操作系統連調度都成問題。
  • 多線程模式通常比多進程快一點,但是也快不到哪裏去,而且多線程致命的缺點是任何一個線程掛掉可能導致整個進程崩潰,因爲所有的線程共享進程的內存。

2.3 線程切換

無論是多進程還是多線程,只要數量一多,效率肯定上不去,爲什麼呢?

我們打個比方,假設你不幸正在準備中考,每天晚上需要做語文、數學、英語、物理、化學這5科的作業,每項作業耗時1小時。

如果你先花1小時做語文作業,做完了,再花1小時做數學作業,這樣,依次全部做完,一共花5小時,這種方式稱爲單任務模型,或者批處理任務模型。

假設你打算切換到多任務模型,可以先做1分鐘語文,再切換到數學作業,做1分鐘,再切換到英語,以此類推,只要切換速度足夠快,這種方式就和單核CPU執行多任務是一樣的了,以幼兒園小朋友的眼光來看,你就正在同時寫5科作業。

但是,切換作業是有代價的,比如從語文切到數學,要先收拾桌子上的語文書本、鋼筆(這叫保存現場),然後,打開數學課本、找出圓規直尺(這叫準備新環境),才能開始做數學作業。操作系統在切換進程或者線程時也是一樣的,它需要先保存當前執行的現場環境(CPU寄存器狀態、內存頁等),然後,把新任務的執行環境準備好(恢復上次的寄存器狀態,切換內存頁等),才能開始執行。這個切換過程雖然很快,但是也需要耗費時間。如果有幾千個任務同時進行,操作系統可能就主要忙着切換任務,根本沒有多少時間去執行任務了,這種情況最常見的就是硬盤狂響,點窗口無反應,系統處於假死狀態。

所以,多任務一旦多到一個限度,就會消耗掉系統所有的資源,結果效率急劇下降,所有任務都做不好。

2.4 計算密集型與IO密集型

是否採用多任務的第二個考慮是任務的類型。我們可以把任務分爲計算密集型和IO密集型。

  • 計算密集型任務

計算密集型任務的特點是要進行大量的計算,消耗CPU資源,比如計算圓周率、對視頻進行高清解碼等等,全靠CPU的運算能力。這種計算密集型任務雖然也可以用多任務完成,但是任務越多,花在任務切換的時間就越多,CPU執行任務的效率就越低,所以,要最高效地利用CPU,計算密集型任務同時進行的數量應當等於CPU的核心數。

計算密集型任務由於主要消耗CPU資源,因此,代碼運行效率至關重要。Python這樣的腳本語言運行效率很低,完全不適合計算密集型任務。對於計算密集型任務,最好用C語言編寫。

  • IO密集型任務

第二種任務的類型是IO密集型,涉及到網絡、磁盤IO的任務都是IO密集型任務,這類任務的特點是CPU消耗很少,任務的大部分時間都在等待IO操作完成(因爲IO的速度遠遠低於CPU和內存的速度)。對於IO密集型任務,任務越多,CPU效率越高,但也有一個限度。常見的大部分任務都是IO密集型任務,比如Web應用。

IO密集型任務執行期間,99%的時間都花在IO上,花在CPU上的時間很少,因此,用運行速度極快的C語言替換用Python這樣運行速度極低的腳本語言,完全無法提升運行效率。對於IO密集型任務,最合適的語言就是開發效率最高(代碼量最少)的語言,腳本語言是首選,C語言最差。

參考文獻

  • 湯子瀛.計算機操作系統(第三版).2007:34-78
  • https://www.jianshu.com/p/5ce909404c90
  • https://www.liaoxuefeng.com/wiki/1016959663602400/1017631469467456
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章