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