Python之多線程與多進程(一)

多線程python

多線程是程序在一樣的上下文中同時運行多條線程的能力。這些線程共享同一個進程的資源,能夠在併發模式(單核處理器)或並行模式(多核處理器)下執行多個任務安全

多線程有如下幾個優勢:bash

  • 持續響應:在單線程的程序中,執行一個長期運行的任務可能會致使程序的凍結。多線程能夠把這個長期運行的任務放在一個線程中,在程序併發的運行任務時能夠持續響應客戶的需求
  • 更快的執行速度:在多核處理器的操做系統上,多線程能夠經過真正的並行提升程序的運行速度
  • 較低的資源消耗:利用線程模式,程序能夠利用一個進程內的資源響應多個請求
  • 更簡單的狀態共享與進程間的通訊機制:因爲線程都共享同一資源和內存空間,所以線程之間的通比進程間通訊簡單
  • 並行化:多處理器系統能夠實現多線程的每一個線程獨立運行

可是多線程也有如下幾個缺點:服務器

  • 線程同步:因爲多個線程是在同一數據上運行的,因此須要引入一些機制預防競態條件
  • 問題線程致使集體崩潰:雖然多個線程能夠獨立運行,但一旦某個線程出現問題,也可能形成整個進程崩潰
  • 死鎖:這是線程操做的常見問題。一般,線程執行任務時會鎖住正在使用的資源,當一個線程開始等待另外一個線程資源釋放,而另外一個線程同時也要等待第一個線程釋放資源時,就發生了死鎖

一般,多線程技術徹底能夠在多處理器上實現並行計算。可是Python的官方版本(CPython)有一個GIL限制,GIL會阻止多個線程同時運行Python的字節碼,這就不是真正的並行了。假如你的系統有6個處理器,多線程能夠把CPU跑到網絡

600%,然而,你能看到的只有100%,甚至更慢一點,這都是GIL形成的多線程

CPython的GIL是有必要的,由於CPython的內存管理不是線程安全的。所以,爲了讓每一個任務都按順序進行,它須要確保運行過程當中內存不被幹擾。它能夠更快的運行單線程程序,簡化C語言擴展庫的使用方法,由於它不須要考慮多線程問題。併發

可是,GIL是能夠用一些辦法繞過的。例如,因爲GIL只阻止多個線程同時運行Python的字節碼,因此能夠用C語言寫程序,而後用Python封裝。這樣,在程序運行過程當中GIL就不會干擾多線程併發了app

另外一個GIL不影響性能的示例就是網絡服務器了,服務器大部分時間都在讀數據包,而當發生IO等待時,會嘗試釋放GIL。這種狀況下,增長線程能夠讀取更多的包,雖然這並非真正的並行。這樣作能夠增長服務器的性能,可是不會影響速度。ide

用_thread模塊建立線程函數

咱們先用一個例子快速演示_thread模塊的用法:_thread模塊提供了start_new_thread方法。咱們能夠向裏面傳入如下參數:

  • 目標函數:裏面包含咱們要運行的代碼,一旦函數返回值,線程就中止運行
  • 參數:即執行目標函數所需的參數,通常以元組的形式傳入
import _thread
import time


def print_time(thread_name, delay):
    count = 0
    while count < 5:
        time.sleep(delay)
        count += 1
        print("%s:%s" % (thread_name, time.ctime(time.time())))


try:
    _thread.start_new_thread(print_time, ("thread-A", 1))
    _thread.start_new_thread(print_time, ("thread-B", 2))
except:
    print("Error: unable to start thread")

while 1:
    pass

  

運行結果:

thread-A:Sun Jul  8 07:39:27 2018
thread-B:Sun Jul  8 07:39:28 2018
thread-A:Sun Jul  8 07:39:28 2018
thread-A:Sun Jul  8 07:39:29 2018
thread-B:Sun Jul  8 07:39:30 2018
thread-A:Sun Jul  8 07:39:30 2018
thread-A:Sun Jul  8 07:39:31 2018
thread-B:Sun Jul  8 07:39:32 2018
thread-B:Sun Jul  8 07:39:34 2018
thread-B:Sun Jul  8 07:39:36 2018

  

上面的例子很簡單,線程A和線程B是併發執行的。

 _thread模塊還提供了一些容易使用的線程原生接口:

  • _thread.interrupt_main():這個方法能夠向主線程發送中斷異常,就像經過鍵盤向程序輸入CTRL+C同樣,咱們修改print_time方法,當count爲2,休眠時間delay爲2向主線程發送中斷異常
    def print_time(thread_name, delay):
        count = 0
        while count < 5:
            time.sleep(delay)
            count += 1
            if count == 2 and delay == 2:
                _thread.interrupt_main()
            print("%s:%s" % (thread_name, time.ctime(time.time())))

    運行結果:

    thread-A:Sun Jul  8 09:12:57 2018
    thread-B:Sun Jul  8 09:12:58 2018
    thread-A:Sun Jul  8 09:12:58 2018
    thread-A:Sun Jul  8 09:12:59 2018
    thread-B:Sun Jul  8 09:13:00 2018
    Traceback (most recent call last):
      File "D:/pypath/hello/test3/test01.py", line 22, in <module>
        pass
    KeyboardInterrupt

        

  • exit:這個方法會從後臺退出程序,它的優勢是中斷線程時不會引發其餘異常
    def print_time(thread_name, delay):
        count = 0
        while count < 5:
            time.sleep(delay)
            count += 1
            if count == 2 and delay == 2:
                _thread.exit()
            print("%s:%s" % (thread_name, time.ctime(time.time())))

    運行結果:

    thread-A:Sun Jul  8 09:15:51 2018
    thread-B:Sun Jul  8 09:15:52 2018
    thread-A:Sun Jul  8 09:15:52 2018
    thread-A:Sun Jul  8 09:15:53 2018
    thread-A:Sun Jul  8 09:15:54 2018
    thread-A:Sun Jul  8 09:15:55 2018

      

allocate_lock方法能夠爲線程返回一個線程鎖,這個鎖能夠保護某一代碼塊從開始運行到運行結束只有一個線程,線程鎖對象有三個方法:

  • acquire:這個方法的主要做用是爲當前的線程請求一把線程鎖。它接受一個可選的整型參數,若是參數是0,那麼線程鎖一旦被請求則當即獲取,不須要等待,若是參數不是0,則表示線程能夠等待鎖
  • release:這個方法會釋放線程鎖,讓下一個線程獲取
  • locked:若是線程鎖被某個線程獲取,就返回True,不然爲False

下面這段代碼用10個線程對一個全局變量增長值,所以,理想狀況下,全局變量的值應該是10:

import _thread
import time

global_values = 0


def run(thread_name):
    global global_values
    local_copy = global_values
    print("%s with value %s" % (thread_name, local_copy))
    global_values = local_copy + 1


for i in range(10):
    _thread.start_new_thread(run, ("thread-(%s)" % str(i),))

time.sleep(3)
print("global_values:%s" % global_values)

  

運行結果:

thread-(0) with value 0
thread-(1) with value 0
thread-(2) with value 0
thread-(4) with value 0
thread-(6) with value 0
thread-(8) with value 0
thread-(7) with value 0
thread-(5) with value 0
thread-(3) with value 0
thread-(9) with value 1
global_values:2

    

可是很遺憾,咱們沒有獲得咱們但願的結果,相反,程序運行的結果和咱們但願的結果差距更遠。形成這樣的緣由,都是由於多個線程操做同一變量或同一代碼塊致使有的線程不能讀到最新的值,甚至是把舊值的運算結果賦給所有局變量

如今,讓咱們修改一下原先的代碼:

import _thread
import time

global_values = 0


def run(thread_name, lock):
    global global_values
    lock.acquire()
    local_copy = global_values
    print("%s with value %s" % (thread_name, local_copy))
    global_values = local_copy + 1
    lock.release()


lock = _thread.allocate_lock()

for i in range(10):
    _thread.start_new_thread(run, ("thread-(%s)" % str(i), lock))

time.sleep(3)
print("global_values:%s" % global_values)

  

運行結果:

thread-(0) with value 0
thread-(2) with value 1
thread-(4) with value 2
thread-(5) with value 3
thread-(3) with value 4
thread-(6) with value 5
thread-(1) with value 6
thread-(7) with value 7
thread-(8) with value 8
thread-(9) with value 9

  

如今能夠看到,線程的執行順序依舊是亂序的,但全局變量的值是逐個遞增的

_thread還有其餘一些方法:

  • _thread.get_ident():這個方法會返回一個非0的整數,表明當前活動線程的id。這個整數會在線程結束或退出後收回,所以在整個程序的生命週期中它並非惟一
  • _thread.stack_size(size):size這個參數是可選項,可在代碼建立新線程時設置或返回線程棧的容量,這個容量能夠是0,或者至少32KB,具體由操做系統決定

用threading模塊建立線程

這是目前Python中處理線程廣泛推薦的模塊,這個模塊提供了更完善和高級的接口,咱們嘗試將前面的示例轉化成threading模塊的形式:

import threading
import time

global_values = 0


def run(thread_name, lock):
    global global_values
    lock.acquire()
    local_copy = global_values
    print("%s with value %s" % (thread_name, local_copy))
    global_values = local_copy + 1
    lock.release()


lock = threading.Lock()

for i in range(10):
    t = threading.Thread(target=run, args=("thread-(%s)" % str(i), lock))
    t.start()

time.sleep(3)
print("global_values:%s" % global_values)

  

對於更復雜的狀況,若是要更好地封裝線程的行爲,咱們可能須要建立本身的線程類,這裏須要注意幾點:

  • 須要繼承thread.Thread類
  • 須要改寫run方法,也可使用__init__方法
  • 若是改寫初始化方法__init__,須要在一開始調用父類的初始化方法Thread.__init__
  • 當線程的run方法中止或拋出未處理的異常時,線程將中止,所以要提早設計好方法
  • 能夠用初始化方法的name參數名稱命名你的線程
import threading
import time


class MyThread(threading.Thread):

    def __init__(self, count):
        threading.Thread.__init__(self)
        self.total = count

    def run(self):
        for i in range(self.total):
            time.sleep(1)
            print("Thread:%s - %s" % (self.name, i))


t = MyThread(2)
t2 = MyThread(3)
t.start()
t2.start()

print("finish")

  

運行結果:

finish
Thread:Thread-2 - 0
Thread:Thread-1 - 0
Thread:Thread-2 - 1
Thread:Thread-1 - 1
Thread:Thread-2 - 2

  

注意上面主線程先打印了finish,以後纔打印其餘線程裏面的print語句,這並非什麼大問題,但下面的狀況就有問題了:

f = open("content.txt", "w+")
t = MyThread(2, f)
t2 = MyThread(3, f)
t.start()
t2.start()
f.close()

  

咱們假設在MyThread中會將打印的語句寫入content.txt,但這段代碼是會出問題的,由於在開啓其餘線程前,主線程可能會先關閉文件處理器,若是想避免這種狀況,應該使用join方法,join方法會使得被調用的線程執行完畢後,在能返回原先的線程繼續執行下去:

f = open("content.txt", "w+")
t = MyThread(2, f)
t2 = MyThread(3, f)
t.start()
t2.start()
t.join()
t2.join()
f.close()
print("finish")

  

join方法還支持一個可選參數:時限(浮點數或None),以秒爲單位。可是join返回值是None。所以,要檢查操做是否已超時,須要在join方法返回後查看線程的激活狀態,若是線程的狀態是激活的,操做就超時了

 

再來看一個示例,它檢查一組網站的請求狀態碼:

from urllib.request import urlopen

sites = [
    "https://www.baidu.com/",
    "http://www.sina.com.cn/",
    "http://www.qq.com/"
]


def check_http_status(url):
    return urlopen(url).getcode()


http_status = {}
for url in sites:
    http_status[url] = check_http_status(url)

for key, value in http_status.items():
    print("%s %s" % (key, value))

  

運行結果:

# time python3 test01.py 
https://www.baidu.com/ 200
http://www.sina.com.cn/ 200
http://www.qq.com/ 200

real	0m1.669s
user	0m0.143s
sys	0m0.026s

  

如今,咱們嘗試着把IO操做函數轉變爲一個線程來優化代碼:

from urllib.request import urlopen
import threading

sites = [
    "https://www.baidu.com/",
    "http://www.sina.com.cn/",
    "http://www.qq.com/"
]


class HttpStatusChecker(threading.Thread):
    def __init__(self, url):
        threading.Thread.__init__(self)
        self.url = url
        self.status = None

    def run(self):
        self.status = urlopen(self.url).getcode()


threads = []

http_status = {}
for url in sites:
    t = HttpStatusChecker(url)
    t.start()
    threads.append(t)

for t in threads:
    t.join()

for t in threads:
    print("%s %s" % (t.url, t.status))

  

運行結果:

# time python3 test01.py 
https://www.baidu.com/ 200
http://www.sina.com.cn/ 200
http://www.qq.com/ 200

real	0m0.237s
user	0m0.110s
sys	0m0.019s

  

顯然,線程版的程序更快,運行速度幾乎是上一版的8倍,性能改善十分顯著

經過Event對象實現線程間通訊

雖然線程一般是做爲獨立運行或並行的任務,可是有時也會出現線程間通訊的需求,threading模塊提供了事件(event)對象實現線程間通訊,它包含一個內部標記,以及可使用set()和clear()方法的調用線程

Event類的接口很簡單,它支持的方法以下:

  • is_set:若是事件設置了內部標記,就返回True
  • set:把內部標記設置爲True。它能夠喚醒等待被設置標記的全部線程,調用wait()方法的線程將再也不被阻塞
  • clear:重置內部標記。調用wait方法的線程,在調用set()方法以前都將被阻塞
  • wait:在事件的內部標記被設置好以前,使用這個方法會一直阻塞線程調用,這個方法支持一個可選參數,做爲等待時限(timeout)。若是等待時限非0,則線程會在時限內被一直阻塞

 讓咱們用線程事件對象來演示一個簡單的線程通訊示例,它們能夠輪流打印字符串。兩個線程共享同一個事件對象。在while循環中,每次循環時,一個線程設置標記,另外一個線程重置標記。

 

import threading
import time


class ThreadA(threading.Thread):
    def __init__(self, event):
        threading.Thread.__init__(self)
        self.event = event

    def run(self):
        count = 0
        while count < 6:
            time.sleep(1)
            if self.event.is_set():
                print("A")
                self.event.clear()
            count += 1


class ThreadB(threading.Thread):
    def __init__(self, event):
        threading.Thread.__init__(self)
        self.event = event

    def run(self):
        count = 0
        while count < 6:
            time.sleep(1)
            if not self.event.is_set():
                print("B")
                self.event.set()
            count += 1


event = threading.Event()
ta = ThreadA(event)
tb = ThreadB(event)
ta.start()
tb.start()

  

運行結果:

B
A
B
A
B
A
B
A
B
A
B

  

下面總結一下Python多線程的使用時機:

使用多線程:

  • 頻繁的IO操做
  • 並行任務能夠經過併發解決
  • GUI開發

不使用多線程:

  • 大量的CPU操做任務
  • 程序必須利用多核心操做系統

 多進程

 因爲GIL的存在,Python的多線程並無實現真正的並行。所以,一些問題使用threading模塊並不能解決

不過Python爲並行提供了一個替代方法:多進程。在多進程裏,線程被換成一個個子進程。每一個進程都運做着各自的GIL(這樣Python就能夠並行開啓多個進程,沒有數量限制)。須要明確的是,線程都是同一個進程的組成部分,它們共享同一塊內存、存儲空間和計算資源。而進程卻不會與它們的父進程共享內存,所以進程間通訊比線程間通訊更爲複雜

多進程相比多線程優缺點以下:

優勢 缺點
可使用多核操做系統 更多的內存消耗
進程使用獨立的內存空間,避免競態問題 進程間的數據共享變得更加困難
子進程容易中斷 進程間通訊比線程困難
避開GIL限制
相關文章
相關標籤/搜索