Python 進程與線程

2017-07-30 19:44:25python

什麼叫「多任務」呢?簡單地說,就是操做系統能夠同時運行多個任務。打個比方,你一邊在用瀏覽器上網,一邊在聽MP3,一邊在用Word趕做業,這就是多任務,至少同時有3個任務正在運行。還有不少任務悄悄地在後臺同時運行着,只是桌面上沒有顯示而已。數據庫

如今,多核CPU已經很是普及了,可是,即便過去的單核CPU,也能夠執行多任務。因爲CPU執行代碼都是順序執行的,那麼,單核CPU是怎麼執行多任務的呢?編程

答案就是操做系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務2,任務2執行0.01秒,再切換到任務3,執行0.01秒……這樣反覆執行下去。表面上看,每一個任務都是交替執行的,可是,因爲CPU的執行速度實在是太快了,咱們感受就像全部任務都在同時執行同樣。瀏覽器

真正的並行執行多任務只能在多核CPU上實現,可是,因爲任務數量遠遠多於CPU的核心數量,因此,操做系統也會自動把不少任務輪流調度到每一個核心上執行。安全

對於操做系統來講,一個任務就是一個進程(Process),好比打開一個瀏覽器就是啓動一個瀏覽器進程,打開一個記事本就啓動了一個記事本進程,打開兩個記事本就啓動了兩個記事本進程,打開一個Word就啓動了一個Word進程。ruby

有些進程還不止同時幹一件事,好比Word,它能夠同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時幹多件事,就須要同時運行多個「子任務」,咱們把進程內的這些「子任務」稱爲線程(Thread)。服務器

因爲每一個進程至少要幹一件事,因此,一個進程至少有一個線程。固然,像Word這種複雜的進程能夠有多個線程,多個線程能夠同時執行,多線程的執行方式和多進程是同樣的,也是由操做系統在多個線程之間快速切換,讓每一個線程都短暫地交替運行,看起來就像同時執行同樣。固然,真正地同時執行多線程須要多核CPU纔可能實現。網絡

咱們前面編寫的全部的Python程序,都是執行單任務的進程,也就是隻有一個線程。若是咱們要同時執行多個任務怎麼辦?多線程

有兩種解決方案:併發

一種是啓動多個進程,每一個進程雖然只有一個線程,但多個進程能夠一塊執行多個任務。

還有一種方法是啓動一個進程,在一個進程內啓動多個線程,這樣,多個線程也能夠一塊執行多個任務。

固然還有第三種方法,就是啓動多個進程,每一個進程再啓動多個線程,這樣同時執行的任務就更多了,固然這種模型更復雜,實際不多采用。

總結一下就是,多任務的實現有3種方式:

  • 多進程模式;
  • 多線程模式;
  • 多進程+多線程模式。

同時執行多個任務一般各個任務之間並非沒有關聯的,而是須要相互通訊和協調,有時,任務1必須暫停等待任務2完成後才能繼續執行,有時,任務3和任務4又不能同時執行,因此,多進程和多線程的程序的複雜度要遠遠高於咱們前面寫的單進程單線程的程序。

由於複雜度高,調試困難,因此,不是無可奈何,咱們也不想編寫多任務。可是,有不少時候,沒有多任務還真不行。想一想在電腦上看電影,就必須由一個線程播放視頻,另外一個線程播放音頻,不然,單線程實現的話就只能先把視頻播放完再播放音頻,或者先把音頻播放完再播放視頻,這顯然是不行的。

Python既支持多進程,又支持多線程。

線程是最小的執行單元,而進程由至少一個線程組成。如何調度進程和線程,徹底由操做系統決定,程序本身不能決定何時執行,執行多長時間。

多進程和多線程的程序涉及到同步、數據共享的問題,編寫起來更復雜。

 

 1、多進程

  • 要實現跨平臺的多進程,可使用multiprocessing模塊中的Process類

若是你打算編寫多進程的服務程序,Unix/Linux無疑是正確的選擇。因爲Windows沒有fork調用,難道在Windows上沒法用Python編寫多進程的程序?因爲Python是跨平臺的,天然也應該提供一個跨平臺的多進程支持。multiprocessing模塊就是跨平臺版本的多進程模塊。multiprocessing模塊提供了一個Process類來表明一個進程對象。

    • 建立子進程時,只須要傳入一個執行函數和函數的參數(元組類型),建立一個Process實例。
    • start()方法啓動,這樣建立進程比fork()還要簡單。
    • join()方法能夠等待子進程結束後再繼續往下運行,一般用於進程間的同步。
from multiprocessing import Process import os def runproc(name): print('This is child process-%s:%s' %(name,os.getpid())) if __name__ == '__main__': print('Parent process name:%s' %os.getpid()) p = Process(target=runproc,args=('child',)) print('Child process will start') p.start() p.join() print('Child process is ended')
# Parent process name:3748
# Child process will start
# This is child process-child:8896
# Child process is ended
  •  若是要啓動大量的子進程,能夠用進程池的方式批量建立子進程,使用Pool類

在使用Python進行系統管理時,特別是同時操做多個文件目錄或者遠程控制多臺主機,並行操做能夠節約大量的時間。若是操做的對象數目不大時,還能夠直接使用Process類動態的生成多個進程,十幾個還好,可是若是上百個甚至更多,那手動去限制進程數量就顯得特別的繁瑣,此時進程池就派上用場了。
Pool類能夠提供指定數量的進程供用戶調用,當有新的請求提交到Pool中時,若是池尚未滿,就會建立一個新的進程來執行請求。若是池滿,請求就會告知先等待,直到池中有進程結束,纔會建立新的進程來執行這些請求。

    • Pool(n) : 表示最多同時執行多少子進程
    • apply_async(func[, args=()[, kwds={}[, callback=None]]]) : 它是非阻塞且支持結果返回進行回調
    • close() : 關閉進程池,使其再也不接受新的任務
    • terminate() : 結束工做進程,不在處理未處理的任務
    • join() : 主進程阻塞,等待子進程的完成。join()方法必須在close()方法或者terminate()方法以後使用。
    • map(func, iterable[, chunksize=None]) : Pool類中的map方法,與內置的map函數用法行爲基本一致,它會使進程阻塞直到返回結果。注意,雖然第二個參數是一個迭代器,但在實際使用中,必須在整個隊列都就緒後,程序纔會運行子進程。Map按序處理這些迭代。調用這個函數,它就會返回給咱們一個按序存儲着結果的簡易列表。
from multiprocessing import Pool import os, time, random def long_time_task(name): print('Run task %s (%s)...' % (name, os.getpid())) start = time.time() time.sleep(random.random() * 3) end = time.time() print('My Parent PID is: %s' %os.getppid()) print('Task %s runs %0.2f seconds.' % (name, (end - start))) if __name__=='__main__': print('Parent process PID: %s.' % os.getpid()) p = Pool(4) for i in range(5): p.apply_async(long_time_task, args=(i,)) print('time: ',time.time()) print('Waiting for all subprocesses done...') print('Parent time: ',time.time()) p.close() p.join() print('All subprocesses done.') # Parent process PID: 23444. # time: 1501498489.5833461 # time: 1501498489.5833461 # time: 1501498489.5833461 # time: 1501498489.5833461 # time: 1501498489.5833461 # Waiting for all subprocesses done... # Parent time: 1501498489.5843477 # Run task 0 (18564)... # Run task 1 (23532)... # Run task 2 (22656)... # Run task 3 (23540)... # My Parent PID is: 23444 # Task 2 runs 0.24 seconds. # Run task 4 (22656)... # My Parent PID is: 23444 # Task 4 runs 0.50 seconds. # My Parent PID is: 23444 # Task 1 runs 1.26 seconds. # My Parent PID is: 23444 # Task 0 runs 2.30 seconds. # My Parent PID is: 23444 # Task 3 runs 2.60 seconds. # All subprocesses done.

一個進程建立子進程以後,進程與產生的進程之間的關係是父子關係,分別成爲進程和子進程。子進程一經產生就與你進程併發執行,子進程共享父進程和子進程。子進程一經產生就與你進程併發執行,子進程共享父進程的正文段和已經打開的文件。父進程和子進程的前後順序由系統調度。這也就是爲何先打印for循環外面的量的緣由。Pool(n)是生成一個至多n個子進程併發的進程池,若是這裏改爲3的話,那麼只會容許前三個task 先執行,執行完了才執行後面的。

import os import time from multiprocessing import Pool def getFile(path) : #獲取目錄下的文件list fileList = [] for root, dirs, files in list(os.walk(path)) : for i in files : if i.endswith('.txt') or i.endswith('.10w') : fileList.append(root + "\\" + i) return fileList def operFile(filePath) : #統計每一個文件中行數和字符數,並返回 filePath = filePath fp = open(filePath) content = fp.readlines() fp.close() lines = len(content) alphaNum = 0 for i in content : alphaNum += len(i.strip('\n')) return lines,alphaNum,filePath def out(list1, writeFilePath) : #將統計結果寫入結果文件中 fileLines = 0 charNum = 0 fp = open(writeFilePath,'a') for i in list1 : fp.write(i[2] + " 行數:"+ str(i[0]) + " 字符數:"+str(i[1]) + "\n") fileLines += i[0] charNum += i[1] fp.close() print fileLines, charNum if __name__ == "__main__": #建立多個進程去統計目錄中全部文件的行數和字符數 startTime = time.time() filePath = "C:\\wcx\\a" fileList = getFile(filePath) pool = Pool(5) resultList =pool.map(operFile, fileList) pool.close() pool.join() writeFilePath = "c:\\wcx\\res.txt" print resultList out(resultList, writeFilePath) endTime = time.time() print "used time is ", endTime - startTime
  •  進程間通訊

Process之間確定是須要通訊的,操做系統提供了不少機制來實現進程間的通訊。Python的multiprocessing模塊包裝了底層的機制,提供了QueuePipes等多種方式來交換數據。

這裏以queue爲例。

from multiprocessing import Process, Queue import os, time, random # 寫數據進程執行的代碼: def write(q): print('Process to write: %s' % os.getpid()) for value in ['A', 'B', 'C']: print('Put %s to queue...' % value) q.put(value) time.sleep(random.random()) # 讀數據進程執行的代碼: def read(q): print('Process to read: %s' % os.getpid()) while True: value = q.get(True) print('Get %s from queue.' % value) if __name__=='__main__': # 父進程建立Queue,並傳給各個子進程: q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) # 啓動子進程pw,寫入:  pw.start() # 啓動子進程pr,讀取:  pr.start() # 等待pw結束:  pw.join() # pr進程裏是死循環,沒法等待其結束,只能強行終止: pr.terminate()
# 結果輸出爲:
'''
Process to write: 50563 Put A to queue... Process to read: 50564 Get A from queue. Put B to queue... Get B from queue. Put C to queue... Get C from queue.
'''

 

2、多線程

多任務能夠由多進程完成,也能夠由一個進程內的多線程完成。咱們前面提到了進程是由若干線程組成的,一個進程至少有一個線程。

因爲線程是操做系統直接支持的執行單元,所以,高級語言一般都內置多線程的支持,Python也不例外,而且,Python的線程是真正的Posix Thread,而不是模擬出來的線程。

Python的標準庫提供了兩個模塊:_threadthreading_thread是低級模塊,threading是高級模塊,對_thread進行了封裝。絕大多數狀況下,咱們只須要使用threading這個高級模塊。

啓動一個線程就是把一個函數傳入並建立Thread實例,而後調用start()開始執行。

多線程相似於同時執行多個不一樣程序,多線程運行有以下優勢:

  • 使用線程能夠把佔據長時間的程序中的任務放到後臺去處理。
  • 用戶界面能夠更加吸引人,這樣好比用戶點擊了一個按鈕去觸發某些事件的處理,能夠彈出一個進度條來顯示處理的進度
  • 程序的運行速度可能加快
  • 在一些等待的任務實現上如用戶輸入、文件讀寫和網絡收發數據等,線程就比較有用了。在這種狀況下咱們能夠釋放一些珍貴的資源如內存佔用等等。

線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。

每一個線程都有他本身的一組CPU寄存器,稱爲線程的上下文,該上下文反映了線程上次運行該線程的CPU寄存器的狀態。

指令指針和堆棧指針寄存器是線程上下文中兩個最重要的寄存器,線程老是在進程獲得上下文中運行的,這些地址都用於標誌擁有線程的進程地址空間中的內存。

  • 線程能夠被搶佔(中斷)。
  • 在其餘線程正在運行時,線程能夠暫時擱置(也稱爲睡眠) -- 這就是線程的退讓。

線程能夠分爲:

  • 內核線程:由操做系統內核建立和撤銷。
  • 用戶線程:不須要內核支持而在用戶程序中實現的線程。

Python3 線程中經常使用的兩個模塊爲:

  • _thread
  • threading(推薦使用)

thread 模塊已被廢棄。用戶可使用 threading 模塊代替。因此,在 Python3 中不能再使用"thread" 模塊。爲了兼容性,Python3 將 thread 重命名爲 "_thread"。

構造方法: 
Thread(group=None, target=None, name=None, args=(), kwargs={})

  1. group: 線程組,目前尚未實現,庫引用中提示必須是None; 
  2. target: 要執行的方法;
  3. name: 線程名; 
  4. args/kwargs: 要傳入方法的參數。

 

Thread類提供瞭如下方法:

run(): 用以表示線程活動的方法。

start():啓動線程活動。

join([time]): 等待至線程停止。這阻塞調用線程直至線程的join() 方法被調用停止-正常退出或者拋出未處理的異常-或者是可選的超時發生。

isAlive(): 返回線程是否活動的。

getName(): 返回線程名。

setName(): 設置線程名。

Threading 模塊提供的經常使用方法: 

threading.currentThread(): 返回當前的線程變量。 

threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啓動後、結束前,不包括啓動前和終止後的線程。 

threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。

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) 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.

因爲任何進程默認就會啓動一個線程,咱們把該線程稱爲主線程,主線程又能夠啓動新的線程,Python的threading模塊有個current_thread()函數,它永遠返回當前線程的實例。主線程實例的名字叫MainThread,子線程的名字在建立時指定,咱們用LoopThread命名子線程。名字僅僅在打印時用來顯示,徹底沒有其餘意義,若是不起名字Python就自動給線程命名爲Thread-1Thread-2……

 

3、線程同步以及線程優先級隊列

多線程編程,模型複雜,容易發生衝突,必須用鎖加以隔離,同時,又要當心死鎖的發生。

Python解釋器因爲設計時有GIL全局鎖,致使了多線程沒法利用多核。多線程的併發在Python中就是一個美麗的夢。

  • 線程同步

多線程和多進程最大的不一樣在於,多進程中,同一個變量,各自有一份拷貝存在於每一個進程中,互不影響,而多線程中,全部變量都由全部線程共享,因此,任何一個變量均可以被任何一個線程修改,所以,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。

爲了不這種狀況,引入了鎖的概念。

鎖有兩種狀態——鎖定和未鎖定。每當一個線程好比"set"要訪問共享數據時,必須先得到鎖定;若是已經有別的線程好比"print"得到鎖定了,那麼就讓線程"set"暫停,也就是同步阻塞;等到線程"print"訪問完畢,釋放鎖之後,再讓線程"set"繼續。

import time, threading # 假定這是你的銀行存款: balance = 0 lock = threading.Lock() def change_it(n): # 先存後取,結果應該爲0: global balance balance = balance + n balance = balance - n def run_thread(n): for i in range(100000): # 先要獲取鎖:  lock.acquire() try: # 放心地改吧:  change_it(n) finally: # 改完了必定要釋放鎖: lock.release()

當多個線程同時執行lock.acquire()時,只有一個線程能成功地獲取鎖,而後繼續執行代碼,其餘線程就繼續等待直到得到鎖爲止。

得到鎖的線程用完後必定要釋放鎖,不然那些苦苦等待鎖的線程將永遠等待下去,成爲死線程。因此咱們用try...finally來確保鎖必定會被釋放。

鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭至尾完整地執行,壞處固然也不少,首先是阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地降低了。其次,因爲能夠存在多個鎖,不一樣的線程持有不一樣的鎖,並試圖獲取對方持有的鎖時,可能會形成死鎖,致使多個線程所有掛起,既不能執行,也沒法結束,只能靠操做系統強制終止。

啓動與CPU核心數量相同的N個線程,在4核CPU上能夠監控到CPU佔用率僅有102%,也就是僅使用了一核。

可是用C、C++或Java來改寫相同的死循環,直接能夠把所有核心跑滿,4核就跑到400%,8核就跑到800%,爲何Python不行呢?

由於Python的線程雖然是真正的線程,但解釋器執行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執行前,必須先得到GIL鎖,而後,每執行100條字節碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個GIL全局鎖實際上把全部線程的執行代碼都給上了鎖,因此,多線程在Python中只能交替執行,即便100個線程跑在100核CPU上,也只能用到1個核。

GIL是Python解釋器設計的歷史遺留問題,一般咱們用的解釋器是官方實現的CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。

因此,在Python中,可使用多線程,但不要期望能有效利用多核。若是必定要經過多線程利用多核,那隻能經過C擴展來實現,不過這樣就失去了Python簡單易用的特色。

不過,也不用過於擔憂,Python雖然不能利用多線程實現多核任務,但能夠經過多進程實現多核任務。多個Python進程有各自獨立的GIL鎖,互不影響。

  • 線程優先級隊列

Python 的 Queue 模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(後入先出)隊列LifoQueue,和優先級隊列 PriorityQueue。

這些隊列都實現了鎖原語,可以在多線程中直接使用,可使用隊列來實現線程間的同步。

Queue 模塊中的經常使用方法:

  • Queue.qsize() 返回隊列的大小
  • Queue.empty() 若是隊列爲空,返回True,反之False
  • Queue.full() 若是隊列滿了,返回True,反之False
  • Queue.full 與 maxsize 大小對應
  • Queue.get([block[, timeout]])獲取隊列,timeout等待時間
  • Queue.get_nowait() 至關Queue.get(False)
  • Queue.put(item) 寫入隊列,timeout等待時間
  • Queue.put_nowait(item) 至關Queue.put(item, False)
  • Queue.task_done() 在完成一項工做以後,Queue.task_done()函數向任務已經完成的隊列發送一個信號
  • Queue.join() 實際上意味着等到隊列爲空,再執行別的操做
import queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q
    def run(self):
        print ("開啓線程:" + self.name)
        process_data(self.name, self.q)
        print ("退出線程:" + self.name)

def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print ("%s processing %s" % (threadName, data))
        else:
            queueLock.release()
        time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1

# 建立新線程
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1

# 填充隊列
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()

# 等待隊列清空
while not workQueue.empty():
    pass

# 通知線程是時候退出
exitFlag = 1

# 等待全部線程完成
for t in threads:
    t.join()
print ("退出主線程")


# 開啓線程:Thread-1
# 開啓線程:Thread-2
# 開啓線程:Thread-3
# Thread-2 processing One
# Thread-3 processing Two
# Thread-1 processing Three
# Thread-1 processing Four
# Thread-2 processing Five
# 退出線程:Thread-1
# 退出線程:Thread-2
# 退出線程:Thread-3
# 退出主線程

 

 

4、線程的局部變量

在多線程環境下,每一個線程都有本身的數據。一個線程使用本身的局部變量比使用全局變量好,由於局部變量只有線程本身能看見,不會影響其餘線程,而全局變量的修改必須加鎖。

可是局部變量也有問題,就是在函數調用的時候,傳遞起來很麻煩:

def process_student(name):
    std = Student(name)
    # std是局部變量,可是每一個函數都要用它,所以必須傳進去:
    do_task_1(std)
    do_task_2(std)

def do_task_1(std):
    do_subtask_1(std)
    do_subtask_2(std)

def do_task_2(std):
    do_subtask_2(std)
    do_subtask_2(std)

每一個函數一層一層調用都這麼傳參數那還得了?用全局變量?也不行,由於每一個線程處理不一樣的Student對象,不能共享。

若是用一個全局dict存放全部的Student對象,而後以thread自身做爲key得到線程對應的Student對象如何?

global_dict = {}

def std_thread(name):
    std = Student(name)
    # 把std放到全局變量global_dict中:
    global_dict[threading.current_thread()] = std
    do_task_1()
    do_task_2()

def do_task_1():
    # 不傳入std,而是根據當前線程查找:
    std = global_dict[threading.current_thread()]
    ...

def do_task_2():
    # 任何函數均可以查找出當前線程的std變量:
    std = global_dict[threading.current_thread()]
    ...

 

這種方式理論上是可行的,它最大的優勢是消除了std對象在每層函數中的傳遞問題,可是,每一個函數獲取std的代碼有點醜。

有沒有更簡單的方式?

ThreadLocal應運而生,不用查找dictThreadLocal幫你自動作這件事:

import threading

# 建立全局ThreadLocal對象:
local_school = threading.local()

def process_student():
    # 獲取當前線程關聯的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
    # 綁定ThreadLocal的student:
    local_school.student = name
    process_student()

t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

# Hello, Alice (in Thread-A) # Hello, Bob (in Thread-B)

 

全局變量local_school就是一個ThreadLocal對象,每一個Thread對它均可以讀寫student屬性,但互不影響。你能夠把local_school當作全局變量,但每一個屬性如local_school.student都是線程的局部變量,能夠任意讀寫而互不干擾,也不用管理鎖的問題,ThreadLocal內部會處理。

能夠理解爲全局變量local_school是一個dict,不但能夠用local_school.student,還能夠綁定其餘變量,如local_school.teacher等等。

ThreadLocal最經常使用的地方就是爲每一個線程綁定一個數據庫鏈接,HTTP請求,用戶身份信息等,這樣一個線程的全部調用到的處理函數均可以很是方便地訪問這些資源。

一個ThreadLocal變量雖然是全局變量,但每一個線程都只能讀寫本身線程的獨立副本,互不干擾。ThreadLocal解決了參數在一個線程中各個函數之間互相傳遞的問題。

 

5、進程與線程的比較

咱們介紹了多進程和多線程,這是實現多任務最經常使用的兩種方式。如今,咱們來討論一下這兩種方式的優缺點。

首先,要實現多任務,一般咱們會設計Master-Worker模式,Master負責分配任務,Worker負責執行任務,所以,多任務環境下,一般是一個Master,多個Worker。

若是用多進程實現Master-Worker,主進程就是Master,其餘進程就是Worker。

若是用多線程實現Master-Worker,主線程就是Master,其餘線程就是Worker。

多進程模式最大的優勢就是穩定性高,由於一個子進程崩潰了,不會影響主進程和其餘子進程。(固然主進程掛了全部進程就全掛了,可是Master進程只負責分配任務,掛掉的機率低)著名的Apache最先就是採用多進程模式。

多進程模式的缺點是建立進程的代價大,在Unix/Linux系統下,用fork調用還行,在Windows下建立進程開銷巨大。另外,操做系統能同時運行的進程數也是有限的,在內存和CPU的限制下,若是有幾千個進程同時運行,操做系統連調度都會成問題。

多線程模式一般比多進程快一點,可是也快不到哪去,並且,多線程模式致命的缺點就是任何一個線程掛掉均可能直接形成整個進程崩潰,由於全部線程共享進程的內存。在Windows上,若是一個線程執行的代碼出了問題,你常常能夠看到這樣的提示:「該程序執行了非法操做,即將關閉」,其實每每是某個線程出了問題,可是操做系統會強制結束整個進程。

在Windows下,多線程的效率比多進程要高,因此微軟的IIS服務器默認採用多線程模式。因爲多線程存在穩定性的問題,IIS的穩定性就不如Apache。爲了緩解這個問題,IIS和Apache如今又有多進程+多線程的混合模式,真是把問題越搞越複雜。

  • 線程切換

不管是多進程仍是多線程,只要數量一多,效率確定上不去,爲何呢?

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

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

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

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

因此,多任務一旦多到一個限度,就會消耗掉系統全部的資源,結果效率急劇降低,全部任務都作很差。

  • 計算密集型 vs. IO密集型

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

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

計算密集型任務因爲主要消耗CPU資源,所以,代碼運行效率相當重要。Python這樣的腳本語言運行效率很低,徹底不適合計算密集型任務。對於計算密集型任務,最好用C語言編寫。

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

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

  • 異步IO

考慮到CPU和IO之間巨大的速度差別,一個任務在執行的過程當中大部分時間都在等待IO操做,單進程單線程模型會致使別的任務沒法並行執行,所以,咱們才須要多進程模型或者多線程模型來支持多任務併發執行。

現代操做系統對IO操做已經作了巨大的改進,最大的特色就是支持異步IO。若是充分利用操做系統提供的異步IO支持,就能夠用單進程單線程模型來執行多任務,這種全新的模型稱爲事件驅動模型,Nginx就是支持異步IO的Web服務器,它在單核CPU上採用單進程模型就能夠高效地支持多任務。在多核CPU上,能夠運行多個進程(數量與CPU核心數相同),充分利用多核CPU。因爲系統總的進程數量十分有限,所以操做系統調度很是高效。用異步IO編程模型來實現多任務是一個主要的趨勢。

對應到Python語言,單線程的異步編程模型稱爲協程,有了協程的支持,就能夠基於事件驅動編寫高效的多任務程序。咱們會在後面討論如何編寫協程。

 

6、分佈式進程

在Thread和Process中,應當優選Process,由於Process更穩定,並且,Process能夠分佈到多臺機器上,而Thread最多隻能分佈到同一臺機器的多個CPU上。

Python的multiprocessing模塊不但支持多進程,其中managers子模塊還支持把多進程分佈到多臺機器上。一個服務進程能夠做爲調度者,將任務分佈到其餘多個進程中,依靠網絡通訊。因爲managers模塊封裝很好,沒必要了解網絡通訊的細節,就能夠很容易地編寫分佈式多進程程序。

舉個例子:若是咱們已經有一個經過Queue通訊的多進程程序在同一臺機器上運行,如今,因爲處理任務的進程任務繁重,但願把發送任務的進程和處理任務的進程分佈到兩臺機器上。怎麼用分佈式進程實現?

原有的Queue能夠繼續使用,可是,經過managers模塊把Queue經過網絡暴露出去,就可讓其餘機器的進程訪問Queue了。

 

 

 

 

本文借鑑廖雪峯老師的官方文檔進行整改,特此說明。

相關文章
相關標籤/搜索