線程,,有時被稱為輕量進(jìn)程,,是程序執(zhí)行流的最小單元。一個標(biāo)準(zhǔn)的線程由線程ID,,當(dāng)前指令指針(PC),,寄存器集合和堆棧組成。線程是進(jìn)程中的一個實體,,是被系統(tǒng)獨立調(diào)度和分派的基本單位,,線程不擁有私有的系統(tǒng)資源,但它可與同屬一個進(jìn)程的其它線程共享進(jìn)程所擁有的全部資源,。一個線程可以創(chuàng)建和撤消另一個線程,,同一進(jìn)程中的多個線程之間可以并發(fā)執(zhí)行,。 線程是程序中一個單一的順序控制流程。進(jìn)程內(nèi)有一個相對獨立的,、可調(diào)度的執(zhí)行單元,,是系統(tǒng)獨立調(diào)度和分派CPU的基本單位指令運(yùn)行時的程序的調(diào)度單位。在單個程序中同時運(yùn)行多個線程完成不同的工作,,稱為多線程,。Python多線程用于I/O操作密集型的任務(wù),如SocketServer網(wǎng)絡(luò)并發(fā),,網(wǎng)絡(luò)爬蟲,。 現(xiàn)代處理器都是多核的,幾核處理器只能同時處理幾個線程,,多線程執(zhí)行程序看起來是同時進(jìn)行,實際上是CPU在多個線程之間快速切換執(zhí)行,,這中間就涉及到上下問切換,,所謂的上下文切換就是指一個線程Thread被分配的時間片用完了之后,,線程的信息被保存起來,CPU執(zhí)行另外的線程,,再到CPU讀取線程Thread的信息并繼續(xù)執(zhí)行Thread的過程,。 線程模塊 Python的標(biāo)準(zhǔn)庫提供了兩個模塊:_thread和threading。_thread 提供了低級別的,、原始的線程以及一個簡單的互斥鎖,,它相比于 threading 模塊的功能還是比較有限的。Threading模塊是_thread模塊的替代,,在實際的開發(fā)中,絕大多數(shù)情況下還是使用高級模塊threading,,因此本書著重介紹threading高級模塊的使用。 Python創(chuàng)建Thread對象語法如下:
主要參數(shù)說明:
Python中實現(xiàn)多線程有兩種方式:函數(shù)式創(chuàng)建線程和創(chuàng)建線程類。 第一種創(chuàng)建線程方式: 創(chuàng)建線程的時候,,只需要傳入一個執(zhí)行函數(shù)和函數(shù)的參數(shù)即可完成threading.Thread實例的創(chuàng)建。下面的例子使用Thread類來產(chǎn)生2個子線程,,然后啟動2個子線程并等待其結(jié)束,,
運(yùn)行腳本得到以下結(jié)果。
運(yùn)行腳本默認(rèn)會啟動一個線程,,把該線程稱為主線程,,主線程有可以啟動新的線程,,Python的threading模塊有個current_thread()函數(shù),,它將返回當(dāng)前線程的示例,。從當(dāng)前線程的示例可以獲得前運(yùn)行線程名字,,核心代碼如下。
啟動一個線程就是把一個函數(shù)和參數(shù)傳入并創(chuàng)建Thread實例,,然后調(diào)用start()開始執(zhí)行
從返回結(jié)果可以看出主線程示例的名字叫MainThread,子線程的名字在創(chuàng)建時指定,本例創(chuàng)建了2個子線程,,名字叫thread1和thread2,。如果沒有給線程起名字,Python就自動給線程命名為Thread-1,Thread-2…等等,。在本例中定義了線程函數(shù)printNum(),打印idx次記錄后退出,,每次打印使用time.sleep()讓程序休眠一段時間。 第二種創(chuàng)建線程方式:創(chuàng)建線程類 直接創(chuàng)建threading.Thread的子類來創(chuàng)建一個線程對象,實現(xiàn)多線程,。通過繼承Thread類,,并重寫Thread類的run()方法,在run()方法中定義具體要執(zhí)行的任務(wù),。在Thread類中,,提供了一個start()方法用于啟動新進(jìn)程,,線程啟動后會自動調(diào)用run()方法,。
運(yùn)行腳本得到以下結(jié)果,。
從返回結(jié)果可以看出,,通過創(chuàng)建Thread類來產(chǎn)生2個線程對象thr1和thr2,重寫Thread類的run()函數(shù),,把業(yè)務(wù)邏輯放入其中,,通過調(diào)用線程對象的start()方法啟動線程。通過調(diào)用線程對象的join()函數(shù),,等待該線程完成,,在繼續(xù)下面的操作。 在本例中,,主線程MainThread等待子線程thread1和thread2線程運(yùn)行結(jié)束后才輸出” MainThread 線程結(jié)束”,。如果子線程thread1和thread2不調(diào)用join()函數(shù),那么主線程MainThread和2個子線程是并行執(zhí)行任務(wù)的,,2個子線程加上join()函數(shù)后,,程序就變成順序執(zhí)行了。所以子線程用到j(luò)oin()的時候,,通常都是主線程等到其他多個子線程執(zhí)行完畢后再繼續(xù)執(zhí)行,,其他的多個子線程并不需要互相等待,。 守護(hù)線程 在線程模塊中,使用子線程對象用到j(luò)oin()函數(shù),,主線程需要依賴子線程執(zhí)行完畢后才繼續(xù)執(zhí)行代碼,。如果子線程不使用join()函數(shù),主線程和子線程是并行運(yùn)行的,,沒有依賴關(guān)系,,主線程執(zhí)行了,子線程也在執(zhí)行,。 在多線程開發(fā)中,,如果子線程設(shè)定為了守護(hù)線程,守護(hù)線程會等待主線程運(yùn)行完畢后被銷毀,。一個主線程可以設(shè)置多個守護(hù)線程,,守護(hù)線程運(yùn)行的前提是,主線程必須存在,,如果主線程不存在了,,守護(hù)線程會被銷毀。 在本例中創(chuàng)建1個主線程3個子線程,,讓主線程和子線程并行執(zhí)行,。內(nèi)容如下。
運(yùn)行腳本得到以下結(jié)果:
從返回結(jié)果可以看出,,當(dāng)前的線程個數(shù)是4,,線程個數(shù)=主線程數(shù) 子線程數(shù),,在本例中有1個主線程和3個子線程,。主線程執(zhí)行完畢后,等待子線程執(zhí)行完畢,,程序才會退出,。 在本例的基礎(chǔ)上,把所有的子線程都設(shè)置為守護(hù)線程,。子線程變成守護(hù)線程后,,只要主線程執(zhí)行完畢,程序不管子線程有沒有執(zhí)行完畢,,程序都會退出,。使用線程對象的setDaemon(True)函數(shù)來設(shè)置守護(hù)線程。
運(yùn)行腳本得到以下結(jié)果,。
從本例的返回結(jié)果可以看出,,主線程執(zhí)行完畢后,,程序不會等待守護(hù)線程執(zhí)行完畢后就退出了。設(shè)置線程對象為守護(hù)線程,,一定要在線程對象調(diào)用start()函數(shù)前設(shè)置,。 多線程的鎖機(jī)制
多個進(jìn)程之間對內(nèi)存中的變量不會產(chǎn)生沖突,,一個進(jìn)程由多個線程組成,,多線程對內(nèi)存中的變量進(jìn)行共享時會產(chǎn)生影響,所以就產(chǎn)生了死鎖問題,,怎么解決死鎖問題是本節(jié)主要介紹的內(nèi)容,。 1、變量的作用域 一般在函數(shù)體外定義的變量稱為全局變量,,在函數(shù)內(nèi)部定義的變量稱為局部變量,。全局變量所有作用域都可讀,局部變量只能在本函數(shù)可讀,。函數(shù)在讀取變量時,,優(yōu)先讀取函數(shù)本身自有的局部變量,再去讀全局變量,。
運(yùn)行腳本得到以下結(jié)果。
如果注釋掉change()函數(shù)里的 global
在本例中在change()函數(shù)外定義的變量balance是全局變量,,在change()函數(shù)內(nèi)定義的變量num是局部變量,全局變量默認(rèn)是可讀的,,可以在任何函數(shù)中使用,,如果需要改變?nèi)肿兞康闹担枰诤瘮?shù)內(nèi)部使用global定義全局變量,,本例中在change()函數(shù)內(nèi)部使用global定義全局變量balance,在函數(shù)里就可以改變?nèi)肿兞苛恕?/p> 在函數(shù)里可以使用全局變量,,但是在函數(shù)里不能改變?nèi)肿兞俊O雽崿F(xiàn)多個線程共享變量,,需要使用全局變量,。在方法里加上全局關(guān)鍵字 global定義全局變量,,多線程才可以修改全局變量來共享變量。 2,、多線程中的鎖 多線程同時修改全局變量時會出現(xiàn)數(shù)據(jù)安全問題,,線程不安全就是不提供數(shù)據(jù)訪問保護(hù),有可能出現(xiàn)多個線程先后更改數(shù)據(jù)造成所得到的數(shù)據(jù)是臟數(shù)據(jù),。在本例中我們生成2個線程同時修改change()函數(shù)里的全局變量balance時,,會出現(xiàn)數(shù)據(jù)不一致問題。 本案例文件名為PythonFullStack\Chapter03\threadDemo03.py,,內(nèi)容如下,。
運(yùn)行以上腳本,,當(dāng)2個線程運(yùn)行次數(shù)達(dá)到500000次時,,會出現(xiàn)以下結(jié)果。
在本例中定義了一個全局變量balance,初始值為100,,當(dāng)啟動2個線程后,,先加后減,理論上balance應(yīng)該為100,。線程的調(diào)度是由操作系統(tǒng)決定的,,當(dāng)線程t1和t2交替執(zhí)行時,只要循環(huán)次數(shù)足夠多,,balance結(jié)果就不一定是100了,。從結(jié)果可以看出,在本例中線程t1和t2同時修改全局變量balance時,,會出現(xiàn)數(shù)據(jù)不一致問題,。 注意 在多線程情況下,所有的全局變量有所有線程共享,。所以,,任何一個變量都可以被任何一個線程修改,因此,,線程之間共享數(shù)據(jù)最大的危險在于多個線程同時改一個變量,,把內(nèi)容給改亂了,。 在多線程情況下,,使用全局變量并不會共享數(shù)據(jù),會出現(xiàn)線程安全問題,。線程安全就是多線程訪問時,,采用了加鎖機(jī)制,當(dāng)一個線程訪問該類的某個數(shù)據(jù)時,,進(jìn)行保護(hù),,其他線程不能進(jìn)行訪問直到該線程讀取完,,其他線程才可使用。不會出現(xiàn)數(shù)據(jù)不一致 在單線程運(yùn)行時沒有代碼安全問題,。寫多線程程序時,,生成一個線程并不代表多線程。在多線程情況下,,才會出現(xiàn)安全問題,。 針對線程安全問題,需要使用”互斥鎖”,,就像數(shù)據(jù)庫里操縱數(shù)據(jù)一樣,,也需要使用鎖機(jī)制。某個線程要更改共享數(shù)據(jù)時,,先將其鎖定,,此時資源的狀態(tài)為“鎖定”,其他線程不能更改,;直到該線程釋放資源,,將資源的狀態(tài)變成“非鎖定”,其他的線程才能再次鎖定該資源,?;コ怄i保證了每次只有一個線程進(jìn)行寫入操作,從而保證了多線程情況下數(shù)據(jù)的正確性,。 互斥鎖的核心代碼如下:
如果要確保balance計算正確,,使用threading.Lock()來創(chuàng)建鎖對象lock,把 lock.acquire()和lock.release()加在同步代碼塊里,,本例的同步代碼塊就是對全局變量balance進(jìn)行先加后減操作,。 當(dāng)某個線程執(zhí)行change()函數(shù)時,通過lock.acquire()獲取鎖,,那么其他線程就不能執(zhí)行同步代碼塊了,,只能等待知道鎖被釋放了,獲得鎖才能執(zhí)行同步代碼塊,。由于鎖只有一個,,無論多少線程,同一個時刻最多只有一個線程持有該鎖,,所以修改全局變量balance不會產(chǎn)生沖突,。改良后的代碼內(nèi)容如下。
在本例中2個線程同時運(yùn)行l(wèi)ock.acquire()時,,只有一個線程能成功的獲取鎖,然后執(zhí)行代碼,其他線程就繼續(xù)等待直到獲得鎖位置,。獲得鎖的線程用完后一定要釋放鎖,,否則其他線程就會一直等待下去,成為死線程,。 在運(yùn)行上面腳本就不會產(chǎn)生輸出信息,,證明代碼是安全的。把 lock.acquire()和lock.release()加在同步代碼塊里,,還要注意鎖的力度不要加的太大了,。第一個線程只有運(yùn)行完了,第二個線程才能運(yùn)行,,所以鎖要在需要同步代碼里加上,。 留言回復(fù)你在機(jī)器學(xué)習(xí)方面做過哪些有趣的應(yīng)用,我們會在留言中隨機(jī)抽取一位讀者免費送出北京大學(xué)出版社出版的《Python 3.x全棧開發(fā)從入門到精通》圖書一本,。通過“拆解式”講解Python全棧開發(fā)全過程,,本書集理論、技術(shù),、案例,、項目開發(fā)經(jīng)驗為一體,通過海量示例展示開發(fā)過程中的重點,、疑點,、難點,是一本寶典式大全教程,。京東年中購物節(jié),,每滿100減50. |
|