天天看點

python基礎16-并發程式設計(1)

從這篇開始我們學習python并發程式設計的内容,之是以把并發程式設計放到最後去說,是因為并發程式設計涉及很多概念,還用到了類,跟之前的内容不同,它是一個系統性的知識,在我們正式寫代碼前我們先梳理下基本概念,然後再進入實際代碼環節,因為要NB,不但要會寫代碼,而且還能了解代碼背後的意義,if  贊同:   我們先來了解下程序和線程的曆史:

我們都知道計算機是由硬體和軟體組成的。硬體中的CPU是計算機的核心,它承擔計算機的所有任務。 作業系統是運作在硬體之上的軟體,是計算機的管理者,它負責資源的管理和配置設定、任務的排程。 程式是運作在系統上的具有某種功能的軟體,比如說浏覽器,音樂播放器等。 每次執行程式的時候,都會完成一定的功能,比如說浏覽器幫我們打開網頁,為了保證其獨立性,就需要一個專門的管理和控制執行程式的資料結構——程序控制塊。 程序就是一個程式在一個資料集上的一次動态執行過程。 程序一般由程式、資料集、程序控制塊三部分組成。我們編寫的程式用來描述程序要完成哪些功能以及如何完成;資料集則是程式在執行過程中所需要使用的資源;程序控制塊用來記錄程序的外部特征,描述程序的執行變化過程,系統可以利用它來控制和管理程序,它是系統感覺程序存在的唯一标志。

在早期的作業系統裡,計算機隻有一個核心,程序執行程式的最小機關,任務排程采用時間片輪轉的搶占式方式進行程序排程。每個程序都有各自的一塊獨立的記憶體,保證程序彼此間的記憶體位址空間的隔離。 随着計算機技術的發展,程序出現了很多弊端,一是程序的建立、撤銷和切換的開銷比較大,二是由于對稱多處理機(對稱多處理機(SymmetricalMulti-Processing)又叫SMP,是指在一個計算機上彙集了一組處理器(多CPU),各CPU之間共享記憶體子系統以及總線結構)的出現,可以滿足多個運作機關,而多程序并行開銷過大。 這個時候就引入了線程的概念。 線程也叫輕量級程序,它是一個基本的CPU執行單元,也是程式執行過程中的最小單元,由線程ID、程式計數器、寄存器集合 和堆棧共同組成。線程的引入減小了程式并發執行時的開銷,提高了作業系統的并發性能。 線程沒有自己的系統資源,隻擁有在運作時必不可少的資源。但線程可以與同屬與同一程序的其他線程共享程序所擁有的其他資源。

以上這段來自網際網路,關于程序和線程說的比較清晰,是以為了讓大家更好的了解,我這邊直接引用了,在了解了程序和線程後,我們再來看Python中的并發實作,不用想,Python中一定有這種類似子產品,沒錯,這個子產品就是threading,  不過學習Python的人都知道,在python中有這麼一個玩意兒,叫GIL,中文名叫全局解釋器鎖,這個鎖能保證同一個時刻隻有一個線程在運作,這個就保證了在python内部是線程安全的,解決了線程間資料一緻性和狀态同步的困難,但問題來了,這問題就是你即使編寫的多線程代碼,運作時其實還是在單線程執行,沒法實作真正的多線程,看到這些是不是心裡哇涼哇涼的, 不過這些大家也不必過多擔心,這裡說的情況是CPU密集型的情況,如果是IO密集型的情況下是允許其它線程在這個線程等待I/O的時候運作的,是以結論是,Python的多線程在多核CPU上,IO密集型的程式能更适合利用多線程。

寫到這,理論部分就完了,接下來進入我們代碼實戰階段,我們從一個實際的運維工作例子中來,比如你們公司有100個站點需要維護(理論上可以測試更多資料,但為了更快示範我隻選了100個),你需要定時檢測這些站點是否可以正常通路,我們以此需求背景來完成這個代碼例子,首先我們用正常方法編寫代碼,然後在用threading子產品實作并發,然後對比看效果,代碼如下:

#!/usr/bin/evn python

import requests

import time

def get_site_code(url):

    r = requests.get(url)

    status = r.status_code

    line = url +  ' ' + str(status)

    with open('/tmp/site_stauts.txt', 'a+') as f:

        f.writelines(line + '\n')

if __name__ == '__main__':

    print 'starting at:', time.ctime()

    for url in open('urls.txt'):

        url = url.strip()

        get_site_code(url)

    print 'Done at:', time.ctime()

判斷一個站點是否正常,最常用的方法就是獲得這個站點的http狀态碼,在這裡我簡化了需求,隻把獲得的狀态碼寫入到了檔案中, 如果要做監控可以讀取這個檔案,如果不是2xx或3xx的,就可以報警了,我們把要檢查的站點寫入urls.txt檔案中,通過for循環,調用get_site_code()函數将獲得的站點狀态碼寫入site_stauts.txt檔案中,加入time子產品主要就是對比先後運作時間,運作結果:

starting at: Sun Oct 22 19:32:23 2017

Done     at: Sun Oct 22 19:32:40 2017

執行完成一共花了17秒時間,接下來我們采用并發方式修改下這個執行代碼,如下:

import threading

    threads = []

        t = threading.Thread(target=get_site_code, args=(url,))

        threads.append(t)

    #print len(threads)

    for i in range(len(threads)):

        threads[i].start()

        threads[i].join()

運作結果如下:

starting at: Sun Oct 22 19:36:49 2017

Done     at: Sun Oct 22 19:36:51 2017

我們看隻用了2秒,快了8倍,看完結果接下來我們說下代碼,在這例子裡我選擇了我個人認為最簡單的方法,就是在執行個體化每個Thread對象的時候傳入了我們定義的函數get_site_code()和需要的參數url, 執行個體化後得到一個Thread的執行個體t,我們把這個t加入線程清單threads中,接下來循環這個清單開始調用start()函數去執行,除了start()函數,我們還用到了join()函數,這個函數允許主線程等待線程結束.

上面的方法沒問題後,我們來看第二個實作方法,我們還可以通過繼承父類 threading.Thread,來實作一個子類,通過執行個體化我們自己的子類來實作并發,這裡我們需要注意的是run方法是父類的一個方法,我們在子類中重新了父類的run方法,代碼實作如下:

class Work(threading.Thread):

    def __init__(self, url):

        #threading.Thread.__init__(self)

        super(Work, self).__init__()

        self.url = url

    def run(self):

        get_site_code(self.url)    

        threads.append(Work(url))

    for i in threads:

        i.start()

        i.join()