python主线程执行_python 并发执行之多线程
正常情況下,我們在啟動一個程序的時候。這個程序會先啟動一個進(jìn)程,啟動之后這個進(jìn)程會拉起來一個線程。這個線程再去處理事務(wù)。也就是說真正干活的是線程,進(jìn)程這玩意只負(fù)責(zé)向系統(tǒng)要內(nèi)存,要資源但是進(jìn)程自己是不干活的。默認(rèn)情況下只有一個進(jìn)程只會拉起來一個線程。
多線程顧名思義,就是同樣在一個進(jìn)程的情況同時拉起來多個線程。上面說了,真正干活的是線程。進(jìn)程與線程的關(guān)系就像是工廠和工人的關(guān)系。那么現(xiàn)在工廠還是一個,但是干活的工人多了。那么效率自然就提高了。因為只有一個進(jìn)程,所以多線程在提高效率的同時,并沒有向系統(tǒng)伸手要更多的內(nèi)存資源。因此使用起來性價比還是很高的。但是多線程雖然不更多的消耗內(nèi)存,但是每個線程卻需要CPU的的參與。
相當(dāng)于工廠雖然廠房就一間,可以有很多的工人干活。但是這些工人怎么干活還得靠廠長來指揮。工人太多了,廠長忙不過來安排一樣效率不高。所以工人(線程)的數(shù)量最好還是在廠長(cpu)的能力(內(nèi)核數(shù))范圍之內(nèi)比較好。
在python中多線程的實現(xiàn)方式有兩種,我的總結(jié)就是一種是函數(shù)形式的。一種是通過自己創(chuàng)建一個類并繼承threading.Thread類來實現(xiàn)的。其實關(guān)于多線程用到模塊,也是有兩種。一種是thread。這個模塊是最原始的多線程模塊,但是這個模塊據(jù)說是比較low的。threading模塊封裝了thread模塊,反正就是比較高級,反正就是沒人用thread寫程序,都用threading!!記住就好~
下面先來介紹第一種,也是我認(rèn)為比較簡單的一種函數(shù)形式的。
先舉個例子看下面的代碼import?time
def?haha(max_num):
"""
隨便定義一個函數(shù),要求用戶輸入一個要打印數(shù)字的最大范圍
輸入之后就會從0開始打印,直到用戶輸入的范圍值
"""
for?i?in?range(max_num):
"""
每次打印一個數(shù)字前要間隔1秒,那么打印10個數(shù)就要耗時10秒
"""
time.sleep(1)
print?i
for?x?in?range(3):
haha(10)
上面的代碼沒什么難度,只是展現(xiàn)一下如果順序執(zhí)行函數(shù)haha()。執(zhí)行三遍需要耗時30秒。因為程序要執(zhí)行完第一個循環(huán)之后才會執(zhí)行第二個循環(huán)。時間是累加的。
現(xiàn)在我們引入多線程的方式執(zhí)行。看看會不會有什么變化。import?threading
import?time
def?haha(max_num):
"""
隨便定義一個函數(shù),要求用戶輸入一個要打印數(shù)字的最大范圍
輸入之后就會從0開始打印,直到用戶輸入的最大范圍
"""
for?i?in?range(max_num):
"""
每次打印一個數(shù)字要間隔1秒,那么打印10個數(shù)就要耗時10秒
"""
time.sleep(1)
print?i
for?x?in?range(3):
"""
這里的rang(3)是要依次啟動三個線程,每個線程都調(diào)用函數(shù)haha()
第一個線程啟動執(zhí)行之后,馬上啟動第二個線程再次執(zhí)行。最后也相當(dāng)
函數(shù)執(zhí)行了3次
"""
#通過threading.Thread方法實例化多線程類
#target后面跟的是函數(shù)的名稱但是不要帶括號也不填寫參數(shù)
#args后面的內(nèi)容才是要傳遞給函數(shù)haha()的參數(shù)。切記參數(shù)一定要以數(shù)組的形式填寫不然會報錯。
t=threading.Thread(target=haha,args=(10,))
#將線程設(shè)置為守護(hù)線程
t.setDaemon(True)
#線程準(zhǔn)備就緒,隨時等候cpu調(diào)度
t.start()
執(zhí)行的結(jié)果是。。。。。。。。。。。。。。什么都沒有發(fā)生!!!!沒有任何輸出。什么情況??!!!是不是代碼有錯誤??!
其實問題就出在t.setDaemon(True) ?這一句上。默認(rèn)不寫這句或者說默認(rèn)設(shè)置的情況這一句應(yīng)該是
t.setDaemon(False)這樣子的。那這一句是什么意思呢?
setDaemon ? 設(shè)置為后臺線程或前臺線程(默認(rèn))
如果是后臺線程,主線程執(zhí)行過程中,后臺線程也在進(jìn)行,主線程執(zhí)行完畢后,后臺線程不 ? ? ? ? ? ? 論成功與否,均停止
如果是前臺線程,主線程執(zhí)行過程中,前臺線程也在進(jìn)行,主線程執(zhí)行完畢后,等待前臺線 ? ? ? ? ? ? 程也執(zhí)行完成后,程序停止
這些什么前臺、后臺、主線程都是什么玩意?聽著是不是特別暈?其實沒有這么復(fù)雜。簡單理解就是如果這個參數(shù)是True,就表示程序流程跑完之后直接就關(guān)閉線程然后退出了,根本不管線程是否執(zhí)行完。從上面的例子可以看出來,我們每執(zhí)行一遍函數(shù)haha()最少也得耗時10秒,哪怕是打印第一個數(shù)字出來也得停頓1秒之后才會輸出。但是程序流程就是拉起來三個線程就結(jié)束了。執(zhí)行啟動線程的3次for循環(huán)可用不了10秒,1秒都用不到就結(jié)束了。所以就出現(xiàn)了我們看到的結(jié)果,程序拉起來3個線程,就結(jié)束了主線程但是此時線程調(diào)用的函數(shù)haha()還沒來得及輸出呢,就被迫跟著程序一起結(jié)束了。
既然找到了原因,我們就來修改一下代碼。把礙事的那部分置為默認(rèn)值或者干脆不寫這一行import?threading
import?time
def?haha(max_num):
for?i?in?range(max_num):
time.sleep(1)
print?i
for?x?in?range(3):
t=threading.Thread(target=haha,args=(5,))
#也可以干脆不寫這一行
t.setDaemon(False)
t.start()
現(xiàn)在運行,就可以看到看起來很亂的執(zhí)行結(jié)果0
00
1
11
2
2
2
3
3
3
4
4
4
其實這就是三個線程并行運行同時輸出,所以把結(jié)果都輸出到一起引起。正是這種亂才整明白了確實三個函數(shù)haha()在同時運行。
如果想讓結(jié)果看起來規(guī)則一些可以考慮使用join()方法import?threading
import?time
def?haha(max_num):
for?i?in?range(max_num):
time.sleep(1)
print?i
for?x?in?range(3):
t=threading.Thread(target=haha,args=(5,))
t.start()
#通過join方法讓線程逐條執(zhí)行
t.join()
這樣執(zhí)行的結(jié)果看起來就美觀了0
1
2
3
4
0
1
2
3
4
0
1
2
3
4
就像注釋所說的那樣,美觀是沒問題了。可是這樣的話雖然創(chuàng)建了多個線程,每個線程卻是依次執(zhí)行的。沒有了并行還要多線程干嘛。這樣和最上面寫的串行執(zhí)行例子就一個效果了。因此join方法不能隨便亂用的。
可是既然有了join()方法它總得有用吧?設(shè)計出來肯定不是為了擺著看的。現(xiàn)在我們再修改一下代碼,看看join()方法到底怎么正確使用。import?threading
import?time
def?haha(max_num):
for?i?in?range(max_num):
time.sleep(1)
print?i
"""
創(chuàng)建一個列表,用于存儲要啟動多線程的實例
"""
threads=[]
for?x?in?range(3):
t=threading.Thread(target=haha,args=(5,))
#把多線程的實例追加入列表,要啟動幾個線程就追加幾個實例
threads.append(t)
for?thr?in?threads:
#把列表中的實例遍歷出來后,調(diào)用start()方法以線程啟動運行
thr.start()
for?thr?in?threads:
"""
isAlive()方法可以返回True或False,用來判斷是否還有沒有運行結(jié)束
的線程。如果有的話就讓主線程等待線程結(jié)束之后最后再結(jié)束。
"""
if?thr.isAlive():
thr.join()
上面學(xué)習(xí)setDaemon()方法的時候我們知道,主線程其實就相當(dāng)于程序的主運行流程。那么程序運行的時候最先啟動的一定就是主線程,主線程負(fù)責(zé)拉起子線程用于干活。我們的例子中運行函數(shù)haha()線程其實都是子線程。因此可以說多線程其實就是多個子線程。那么程序運行完最后一個退出的也肯定就是主線程。因此上例中最后再遍歷一個遍threads列表的目的就是查看還是否有沒有退出的子線程,只要還有子線程是活的,沒有退出。就通過join()方法強制程序流程不可以走到主線程退出的那個步驟。只有等子線程都退出之后,才能根據(jù)join()方法的規(guī)則順序執(zhí)行到主線程退出的步驟。
第二種創(chuàng)建多線程的方式就是通過自定義一個類來實現(xiàn)的。import?threading
import?time
class?haha(threading.Thread):
"""
自定義一個類haha,必須要繼承threading.Thread,下面必須要重寫一個run()方法。
把要執(zhí)行的函數(shù)寫到run()方法里。如果沒有run()方法就會報錯。其實這個類的作用就是
通過haha類里面的run()方法來定義每個啟動的子線程要執(zhí)行的函數(shù)內(nèi)容。
"""
def?__init__(self,max_num):
threading.Thread.__init__(self)
self.max_num=max_num
def?run(self):
for?i?in?range(self.max_num):
time.sleep(1)
print?i
if?__name__=='__main__':
threads=[]
for?x?in?range(3):
"""
只是這里和函數(shù)方式有點區(qū)別,因為haha類繼承了threading.Thread,所以通過haha類的實例化
就相當(dāng)于調(diào)用了多線程的實例化。剩下的操作就和函數(shù)方式一個樣子了。
"""
t=haha(5)
threads.append(t)
for?thr?in?threads:
thr.start()
for?thr?in?threads:
if?thr.isAlive():
thr.join()
以上就是實現(xiàn)多線程的兩種方式,根據(jù)個人喜好選擇就好。沒什么本質(zhì)區(qū)別。
下面介紹一下線程鎖,先看下面一段代碼import?threading
#定義一個變量
gnum=0
def?work(max_number):
for?i?in?range(max_number):
print?i
def?mylock():
global?gnum
"""
這個函數(shù)運行的時候需要先運行一下函數(shù)work()
執(zhí)行完之后將全局的gnum+1
"""
work(10)
#將變量聲明為全局變量
gnum=gnum+1
print?'gnum?is?',gnum
for?x?in?range(5):
"""
同時啟動5個現(xiàn)成運行mylock()函數(shù)
"""
t=threading.Thread(target=mylock)
t.start()
上面的例子看起來也不難,目的就是在執(zhí)行g(shù)num+1之前先運行另外一個耗時的函數(shù)而已。因為我們啟動了5個線程同時運行,理論上運行流程應(yīng)該是第一個線程運行完成之后gnum+1=1,此時第二個線程也運行完了在gnum=1的基礎(chǔ)上再加1,使gnum=2。以此類推,最后當(dāng)5個線程運行完了的時候gnum應(yīng)該等于5。但是實際運行的時候并不是我們想象的那個樣子!!!!!
真實的情況是當(dāng)我們第一個線程運行的時候gnum=0,運行一個耗時的work()函數(shù)。因為線程是并發(fā)執(zhí)行的,那這時候在第一個work()還沒運行完的情況下,第二個線程又啟動開始運行了。第一個線程沒有運行完的情況下,是不會執(zhí)行g(shù)num+1操作的。此時對第二個線程來說依舊是gnum=0。之后第一個線程結(jié)束的時候gnum經(jīng)過自加1變成了gnum=1,可是第二個線程還是當(dāng)初取值的時候還是按照gnum=0來進(jìn)行的自加運算。所以第二次運算的結(jié)果很有可能還是gnum=1。沒有達(dá)到我們理想的gnum=2的效果。
從這里就可以看出來,如果多線程執(zhí)行的任務(wù)互不相干那自然什么事情都沒有。一旦要利用多線程多同一個變量進(jìn)行操作的時候,因為線程是并發(fā)執(zhí)行的。所以很有很可能同時修改變量,導(dǎo)致最終結(jié)果不符合我們的預(yù)期。
遇到這種情況一個方案就是用我們上面跳到j(luò)oin方法,讓線程依次運行。這樣同時就只有一個線程在修改變量,不會出現(xiàn)混亂。但是問題還是一樣多線程并發(fā)的效果就沒有了。肯定不可取。第二個
方案就是使用線程鎖。什么是線程鎖呢?就是在多個線程同時操作一個資源的時候,哪個線程先操作。哪個線程就先鎖定這個資源。直到這個線程操作結(jié)束打開鎖之后,其他的線程才能再操作。這就叫做線程安全,也就是線程鎖。聽起來好像和join()方法有點類似。其實還是有區(qū)別的,先來看看加了線程鎖的代碼。import?threading
gnum=0
lock=threading.RLock()
def?work(max_number):
for?i?in?range(max_number):
print?i
def?mylock():
work(10)
#在操作gnum之前先上鎖
#acquire()的括號里可以定義鎖定的timeout時間,超過這個時間就自動打開鎖
lock.acquire()
global?gnum
gnum=gnum+1
#操作結(jié)束之后再打開鎖
lock.release()
print?'gnum?is?',gnum
for?x?in?range(5):
t=threading.Thread(target=mylock)
t.start()
上從面的代碼可以看出區(qū)別,join()方法是對整個線程做限制。而線程鎖lock.acquire是在線程執(zhí)行過程中對某一部分進(jìn)行鎖限制。例子中被啟動的各個線程還是可以并行運行work()這個比較耗時的函數(shù),只是在gnum的處理上才會受到鎖的限制而已。這樣就解決了多線程同時操作一個資源引發(fā)錯誤數(shù)據(jù)的問題。另外一個要注意的就是threading.RLock()也是Lock()的高級用法,用這個高級的就可以了。
多線程的event事件
一般情況下,多線程在創(chuàng)建之后就開始立即投入工作。沒有任何停頓。但是有時候我們也許并不希望如此。比如我們要寫一個爬蟲程序。在爬取網(wǎng)頁之前,我希望先ping一下這個網(wǎng)頁。看看這個網(wǎng)頁網(wǎng)頁是否可以ping通。如果通了就釋放線程去爬取內(nèi)容。如果不通就去測試下一個網(wǎng)頁。所以python線程的事件用于主線程控制其他線程的執(zhí)行,事件主要提供了三個方法?set、wait、clear。其中event.wait()相當(dāng)于一個全局的標(biāo)識,程序根據(jù)event.set()和event.clear()兩個方法分別定制這個全局Flag的值為True或者Flase。當(dāng)Flag=True的時候就相當(dāng)于收到釋放所有線程的信號。看下面一個列子import?threading
def?do(event):
print?'start'
#函數(shù)執(zhí)行到這里等待信號放行信號
event.wait()
#收到放行信號后執(zhí)行下面的語句
print?'execute'
#實例化threading.Event()事件
event_obj?=?threading.Event()
for?i?in?range(10):
t?=?threading.Thread(target=do,?args=(event_obj,))
t.start()
#先將Flag標(biāo)識置為False
event_obj.clear()
inp?=?raw_input('input:')
#如果用戶輸入'true'就像wait()發(fā)送放行信號
if?inp?==?'true':
event_obj.set()
這樣就完成通過set()和clear()方法控制線程運行的目的
最后再簡單介紹一下GIL
GIL是python的全局解釋器鎖的簡稱。這個鎖是干什么用的呢?說白了就是限制python解釋調(diào)用cpu內(nèi)核之用的。多線程理論上可以同時調(diào)用多個cpu內(nèi)核同時工作,比如java語言就可以做到。但是python因為GIL的存在,同一時間只有一條進(jìn)程在cpu內(nèi)核中進(jìn)行處理。雖然我們可以看到多線程并發(fā)運行,但是那只是因為cpu內(nèi)核通過上下文的切換快速將多個線程來回執(zhí)行造成的假象。python和java那種可以真正調(diào)用多核心多線程的語言,在效率上還是有差異的。這個就是python一直被人詬病的GIL鎖。
總結(jié)
以上是生活随笔為你收集整理的python主线程执行_python 并发执行之多线程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 廖的python教程_学廖老师的pyth
- 下一篇: python写自动答题脚本_问卷星的自动