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