1. 程式人生 > >python多執行緒與多程序及其區別

python多執行緒與多程序及其區別

個人一直覺得對學習任何知識而言,概念是相當重要的。掌握了概念和原理,細節可以留給實踐去推敲。掌握的關鍵在於理解,通過具體的例項和實際操作來感性的體會概念和原理可以起到很好的效果。本文通過一些具體的例子簡單介紹一下python的多執行緒和多程序,後續會寫一些程序通訊和執行緒通訊的一些文章。

python多執行緒

python中提供兩個標準庫thread和threading用於對執行緒的支援,python3中已放棄對前者的支援,後者是一種更高層次封裝的執行緒庫,接下來均以後者為例。

建立執行緒

python中有兩種方式實現執行緒:

  1. 例項化一個threading.Thread的物件,並傳入一個初始化函式物件(initial function )作為執行緒執行的入口;
  2. 繼承threading.Thread,並重寫run函式;
  • 方式1:建立threading.Thread物件
import threading
import time

def tstart(arg):
    time.sleep(0.5)
    print("%s running...." % arg)

if __name__ == '__main__':
    t1 = threading.Thread(target=tstart, args=('This is thread 1',))
    t2 = threading.Thread(target=tstart, args=('This is thread 2',))
    t1.start()
    t2.start()
    print("This is main function")

結果:

This is main function
This is thread 2 running....
This is thread 1 running....
View Code
  • 方式2:繼承threading.Thread,並重寫run
import threading
import time

class CustomThread(threading.Thread):
    def __init__(self, thread_name):
        # step 1: call base __init__ function
        super(CustomThread, self).__init__(name=thread_name)
        self._tname = thread_name

    def run(self):
        # step 2: overide run function
        time.sleep(0.5)
        print("This is %s running...." % self._tname)

if __name__ == "__main__":
    t1 = CustomThread("thread 1")
    t2 = CustomThread("thread 2")
    t1.start()
    t2.start()
    print("This is main function")

 執行結果同方式1.

threading.Thread

上面兩種方法本質上都是直接或者間接使用threading.Thread類

threading.Thread(group=Nonetarget=Nonename=Noneargs=()kwargs={})

關聯上面兩種建立執行緒的方式:

import threading
import time

class CustomThread(threading.Thread):
    def __init__(self, thread_name, target = None):
        # step 1: call base __init__ function
        super(CustomThread, self).__init__(name=thread_name, target=target, args = (thread_name,))
        self._tname = thread_name

    def run(self):
        # step 2: overide run function
        # time.sleep(0.5)
        # print("This is %s running....@run" % self._tname)
        super(CustomThread, self).run()

def target(arg):
    time.sleep(0.5)
    print("This is %s running....@target" % arg)

if __name__ == "__main__":
    t1 = CustomThread("thread 1", target)
    t2 = CustomThread("thread 2", target)
    t1.start()
    t2.start()
    print("This is main function")

結果:

This is main function
This is thread 1 running....@target
This is thread 2 running....@target

上面這段程式碼說明:

  1. 兩種方式建立執行緒,指定的引數最終都會傳給threading.Thread類;
  2. 傳給執行緒的目標函式是在基類Thread的run函式體中被呼叫的,如果run沒有被重寫的話。

threading模組的一些屬性和方法可以參照官網,這裡重點介紹一下threading.Thread物件的方法

下面是threading.Thread提供的執行緒物件方法和屬性:

  • start():建立執行緒後通過start啟動執行緒,等待CPU排程,為run函式執行做準備;
  • run():執行緒開始執行的入口函式,函式體中會呼叫使用者編寫的target函式,或者執行被過載的run函式;
  • join([timeout]):阻塞掛起呼叫該函式的執行緒,直到被呼叫執行緒執行完成或超時。通常會在主執行緒中呼叫該方法,等待其他執行緒執行完成。
  • name、getName()&setName():執行緒名稱相關的操作;
  • ident:整數型別的執行緒識別符號,執行緒開始執行前(呼叫start之前)為None;
  • isAlive()、is_alive():start函式執行之後到run函式執行完之前都為True;
  • daemon、isDaemon()&setDaemon():守護執行緒相關;

這些是我們建立執行緒之後通過執行緒物件對執行緒進行管理和獲取執行緒資訊的方法。

 多執行緒執行

 在主執行緒中建立若執行緒之後,他們之間沒有任何協作和同步,除主執行緒之外每個執行緒都是從run開始被執行,直到執行完畢。

join

我們可以通過join方法讓主執行緒阻塞,等待其建立的執行緒執行完成。

import threading
import time

def tstart(arg):
    print("%s running....at: %s" % (arg,time.time()))
    time.sleep(1)
    print("%s is finished! at: %s" % (arg,time.time()))

if __name__ == '__main__':
    t1 = threading.Thread(target=tstart, args=('This is thread 1',))
    t1.start()
    t1.join()   # 當前執行緒阻塞,等待t1執行緒執行完成
    print("This is main function at:%s" % time.time())

結果:

This is thread 1 running....at: 1564906617.43
This is thread 1 is finished! at: 1564906618.43
This is main function at:1564906618.43

如果不加任何限制,當主執行緒執行完畢之後,當前程式並不會結束,必須等到所有執行緒都結束之後才能結束當前程序。

將上面程式中的t1.join()去掉,執行結果如下:

This is thread 1 running....at: 1564906769.52
This is main function at:1564906769.52
This is thread 1 is finished! at: 1564906770.52

可以通過將建立的執行緒指定為守護執行緒(daemon),這樣主執行緒執行完畢之後會立即結束未執行完的執行緒,然後結束程式。

deamon守護執行緒

import threading
import time

def tstart(arg):
    print("%s running....at: %s" % (arg,time.time()))
    time.sleep(1)
    print("%s is finished! at: %s" % (arg,time.time()))

if __name__ == '__main__':
    t1 = threading.Thread(target=tstart, args=('This is thread 1',))
    t1.setDaemon(True)
    t1.start()
    # t1.join()   # 當前執行緒阻塞,等待t1執行緒執行完成
    print("This is main function at:%s" % time.time())

結果:

This is thread 1 running....at: 1564906847.85
This is main function at:1564906847.85

python多程序

相比較於threading模組用於建立python多執行緒,python提供multiprocessing用於建立多程序。先看一下建立程序的兩種方式。

The multiprocessing package mostly replicates the API of the threading module.  —— python doc

建立程序

建立程序的方式和建立執行緒的方式類似:

  1. 例項化一個multiprocessing.Process的物件,並傳入一個初始化函式物件(initial function )作為新建程序執行入口;
  2. 繼承multiprocessing.Process,並重寫run函式;
  • 方式1:
from multiprocessing import Process  
import os, time

def pstart(name):
    # time.sleep(0.1)
    print("Process name: %s, pid: %s "%(name, os.getpid()))

if __name__ == "__main__": 
    subproc = Process(target=pstart, args=('subprocess',))  
    subproc.start()  
    subproc.join()
    print("subprocess pid: %s"%subproc.pid)
    print("current process pid: %s" % os.getpid())

結果:

Process name: subprocess, pid: 4888 
subprocess pid: 4888
current process pid: 9912
  • 方式2:
from multiprocessing import Process  
import os, time

class CustomProcess(Process):
    def __init__(self, p_name, target=None):
        # step 1: call base __init__ function()
        super(CustomProcess, self).__init__(name=p_name, target=target, args=(p_name,))

    def run(self):
        # step 2:
        # time.sleep(0.1)
        print("Custom Process name: %s, pid: %s "%(self.name, os.getpid()))

if __name__ == '__main__':
    p1 = CustomProcess("process_1")
    p1.start()
    p1.join()
    print("subprocess pid: %s"%p1.pid)
    print("current process pid: %s" % os.getpid())

這裡可以思考一下,如果像多執行緒一樣,存在一個全域性的變數share_data,不同程序同時訪問share_data會有問題嗎?

由於每一個程序擁有獨立的記憶體地址空間且互相隔離,因此不同程序看到的share_data是不同的、分別位於不同的地址空間,同時訪問不會有問題。這裡需要注意一下。

Subprocess模組

既然說道了多程序,那就順便提一下另一種建立程序的方式。

python提供了Sunprocess模組可以在程式執行過程中,呼叫外部的程式。

如我們可以在python程式中開啟記事本,開啟cmd,或者在某個時間點關機:

>>> import subprocess
>>> subprocess.Popen(['cmd'])
<subprocess.Popen object at 0x0339F550>
>>> subprocess.Popen(['notepad'])
<subprocess.Popen object at 0x03262B70>
>>> subprocess.Popen(['shutdown', '-p'])

或者使用ping測試一下網路連通性:

>>> res = subprocess.Popen(['ping', 'www.cnblogs.com'], stdout=subprocess.PIPE).communicate()[0]
>>> print res
正在 Ping www.cnblogs.com [101.37.113.127] 具有 32 位元組的資料:

來自 101.37.113.127 的回覆: 位元組=32 時間=1ms TTL=91
來自 101.37.113.127 的回覆: 位元組=32 時間=1ms TTL=91
來自 101.37.113.127 的回覆: 位元組=32 時間=1ms TTL=91
來自 101.37.113.127 的回覆: 位元組=32 時間=1ms TTL=91

101.37.113.127 的 Ping 統計資訊:
資料包: 已傳送 = 4,已接收 = 4,丟失 = 0 (0% 丟失),
往返行程的估計時間(以毫秒為單位):
最短 = 1ms,最長 = 1ms,平均 = 1ms

python多執行緒與多程序比較

先來看兩個例子:

開啟兩個python執行緒分別做一億次加一操作,和單獨使用一個執行緒做一億次加一操作:

def tstart(arg):
    var = 0
    for i in xrange(100000000):
        var += 1

if __name__ == '__main__':
    t1 = threading.Thread(target=tstart, args=('This is thread 1',))
    t2 = threading.Thread(target=tstart, args=('This is thread 2',))
    start_time = time.time()
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("Two thread cost time: %s" % (time.time() - start_time))
    start_time = time.time()
    tstart("This is thread 0")
    print("Main thread cost time: %s" % (time.time() - start_time))

結果:

Two thread cost time: 20.6570000648
Main thread cost time: 2.52800011635

上面的例子如果只開啟t1和t2兩個執行緒中的一個,那麼執行時間和主執行緒基本一致。這個後面會解釋原因。

使用兩個程序進行上面的操作:

def pstart(arg):
    var = 0
    for i in xrange(100000000):
        var += 1

if __name__ == '__main__':
    p1 = Process(target = pstart, args = ("1", ))
    p2 = Process(target = pstart, args = ("2", ))
    start_time = time.time()
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print("Two process cost time: %s" % (time.time() - start_time))
    start_time = time.time()
    pstart("0")
    print("Current process cost time: %s" % (time.time() - start_time))

結果:

Two process cost time: 2.91599988937
Current process cost time: 2.52400016785

對比分析

雙程序並行執行和單程序執行相同的運算程式碼,耗時基本相同,雙程序耗時會稍微多一些,可能的原因是程序建立和銷燬會進行系統呼叫,造成額外的時間開銷。

但是對於python執行緒,雙執行緒並行執行耗時比單執行緒要高的多,效率相差近10倍。如果將兩個並行執行緒改成序列執行,即:

    t1.start()
    t1.join()
    t2.start()
    t2.join()
    #Two thread cost time: 5.12199997902
    #Main thread cost time: 2.54200005531

可以看到三個執行緒序列執行,每一個執行的時間基本相同。

本質原因雙執行緒是併發執行的,而不是真正的並行執行。原因就在於GIL鎖。

GIL鎖

提起python多執行緒就不得不提一下GIL(Global Interpreter Lock 全域性直譯器鎖),這是目前佔統治地位的python直譯器CPython中為了保證資料安全所實現的一種鎖。不管程序中有多少執行緒,只有拿到了GIL鎖的執行緒才可以在CPU上執行,即時是多核處理器。對一個程序而言,不管有多少執行緒,任一時刻,只會有一個執行緒在執行。對於CPU密集型的執行緒,其效率不僅僅不高,反而有可能比較低。python多執行緒比較適用於IO密集型的程式。對於的確需要並行執行的程式,可以考慮多程序。

多執行緒對鎖的爭奪,CPU對執行緒的排程,執行緒之間的切換等均會有時間開銷。

執行緒與程序區別

下面簡單的比較一下執行緒與程序

  • 程序是資源分配的基本單位,執行緒是CPU執行和排程的基本單位;
  • 通訊/同步方式:
    • 程序:
      • 通訊方式:管道,FIFO,訊息佇列,訊號,共享記憶體,socket,stream流;
      • 同步方式:PV訊號量,管程
    • 執行緒:
      • 同步方式:互斥鎖,遞迴鎖,條件變數,訊號量
      • 通訊方式:位於同一程序的執行緒共享程序資源,因此執行緒間沒有類似於程序間用於資料傳遞的通訊方式,執行緒間的通訊主要是用於執行緒同步。
  • CPU上真正執行的是執行緒,執行緒比程序輕量,其切換和排程代價比程序要小;
  • 執行緒間對於共享的程序資料需要考慮執行緒安全問題,由於程序之間是隔離的,擁有獨立的記憶體空間資源,相對比較安全,只能通過上面列出的IPC(Inter-Process Communication)進行資料傳輸;
  • 系統有一個個程序組成,每個程序包含程式碼段、資料段、堆空間和棧空間,以及作業系統共享部分 ,有等待,就緒和執行三種狀態;
  • 一個程序可以包含多個執行緒,執行緒之間共享程序的資源(檔案描述符、全域性變數、堆空間等),暫存器變數和棧空間等是執行緒私有的;
  • 作業系統中一個程序掛掉不會影響其他程序,如果一個程序中的某個執行緒掛掉而且OS對執行緒的支援是多對一模型,那麼會導致當前程序掛掉;
  • 如果CPU和系統支援多執行緒與多程序,多個程序並行執行的同時,每個程序中的執行緒也可以並行執行,這樣才能最大限度的榨取硬體的效能;

執行緒和程序的上下文切換

程序切換過程切換牽涉到非常多的東西,暫存器內容儲存到任務狀態段TSS,切換頁表,堆疊等。簡單來說可以分為下面兩步:

  1. 頁全域性目錄切換,使CPU到新程序的線性地址空間定址;
  2. 切換核心態堆疊和硬體上下文,硬體上下文包含CPU暫存器的內容,存放在TSS中;

執行緒運行於程序地址空間,切換過程不涉及到空間的變換,只牽涉到第二步;

使用多執行緒還是多程序?

CPU密集型:程式需要佔用CPU進行大量的運算和資料處理;

I/O密集型:程式中需要頻繁的進行I/O操作;例如網路中socket資料傳輸和讀取等;

由於python多執行緒並不是並行執行,因此較適合與I/O密集型程式,多程序並行執行適用於CPU密集型程