在使用多線程之前,我們首頁要理解什么是進程和線程。
什么是進程?
計算機程序只不過是磁盤中可執行的,二進制(或其它類型)的數據。它們只有在被讀取到內存中,被操作系統調用的時候才開始它們的生命期。進程(有時被稱為重量級進程)是程序的一次執行。每個進程都有自己的地址空間,內存,數據棧以及其它記錄其運行軌跡的輔助數據。操作系統管理在其上運行的所有進程,並為這些進程公平地分配時間。
什么是線程?
線程(有時被稱為輕量級進程)跟進程有些相似,不同的是,所有的線程運行在同一個進程中,共享相同的運行環境。我們可以想像成是在主進程或“主線程”中並行運行的“迷你進程”。
7.2.1、單線程
在單線程中順序執行兩個循環。一定要一個循環結束后,另一個才能開始。總時間是各個循環運行時間之和。
onetherad.py
from time import sleep, ctime def loop0(): print 'start loop 0 at:', ctime() sleep(4) print 'loop 0 done at:', ctime() def loop1(): print 'start loop 1 at:', ctime() sleep(2) print 'loop 1 done at:', ctime() def main(): print 'start:', ctime() loop0() loop1() print 'all end:', ctime() if __name__ == '__main__': main()
運行結果:
start loop 0 at: Mon Dec 23 09:59:44 2013 loop 0 done at: Mon Dec 23 09:59:48 2013 start loop 1 at: Mon Dec 23 09:59:48 2013 loop 1 done at: Mon Dec 23 09:59:50 2013 all end: Mon Dec 23 09:59:50 2013
Python通過兩個標准庫thread和threading提供對線程的支持。thread提供了低級別的、原始的線程以及一個簡單的鎖。threading基於Java的線程模型設計。鎖(Lock)和條件變量(Condition)在Java中是對象的基本行為(每一個對象都自帶了鎖和條件變量),而在Python中則是獨立的對象。
7.2.1、thread模塊
mtsleep1.py
import thread from time import sleep, ctime loops = [4,2] def loop0(): print 'start loop 0 at:', ctime() sleep(4) print 'loop 0 done at:', ctime() def loop1(): print 'start loop 1 at:', ctime() sleep(2) print 'loop 1 done at:', ctime() def main(): print 'start:', ctime() thread.start_new_thread(loop0, ()) thread.start_new_thread(loop1, ()) sleep(6) print 'all end:', ctime() if __name__ == '__main__': main()
start_new_thread()要求一定要有前兩個參數。所以,就算我們想要運行的函數不要參數,我們也要傳一個空的元組。
這個程序的輸出與之前的輸出大不相同,之前是運行了 6,7 秒,而現在則是 4 秒,是最長的循環的運行時間與其它的代碼的時間總和。
運行結果:
start: Mon Dec 23 10:05:09 2013 start loop 0 at: Mon Dec 23 10:05:09 2013 start loop 1 at: Mon Dec 23 10:05:09 2013 loop 1 done at: Mon Dec 23 10:05:11 2013 loop 0 done at: Mon Dec 23 10:05:13 2013 all end: Mon Dec 23 10:05:15 2013
睡眠 4 秒和 2 秒的代碼現在是並發執行的。這樣,就使得總的運行時間被縮短了。你可以看到,loop1 甚至在 loop0 前面就結束了。
程序的一大不同之處就是多了一個“sleep(6)”的函數調用。如果我們沒有讓主線程停下來,那主線程就會運行下一條語句,顯示“all end”,然后就關閉運行着 loop0()和 loop1()的兩個線程並退出了。我們使用 6 秒是因為我們已經知道,兩個線程(你知道,一個要 4 秒,一個要 2 秒)在主線程等待 6 秒后應該已經結束了。
你也許在想,應該有什么好的管理線程的方法,而不是在主線程里做一個額外的延時 6 秒的操作。因為這樣一來,我們的總的運行時間並不比單線程的版本來得少。而且,像這樣使用 sleep()函數做線程的同步操作是不可靠的。如果我們的循環的執行時間不能事先確定的話,那怎么辦呢?這可能造成主線程過早或過晚退出。這就是鎖的用武之地了。
mtsleep2.py
#coding=utf-8 import thread from time import sleep, ctime loops = [4,2] def loop(nloop, nsec, lock): print 'start loop', nloop, 'at:', ctime() sleep(nsec) print 'loop', nloop, 'done at:', ctime() #解鎖 lock.release() def main(): print 'starting at:', ctime() locks =[] #以loops數組創建列表,並賦值給nloops nloops = range(len(loops)) for i in nloops: lock = thread.allocate_lock() #鎖定 lock.acquire() #追加到locks[]數組中 locks.append(lock) #執行多線程 for i in nloops: thread.start_new_thread(loop,(i,loops[i],locks[i])) for i in nloops: while locks[i].locked(): pass print 'all end:', ctime() if __name__ == '__main__': main()
thread.allocate_lock()
返回一個新的鎖定對象。
acquire() /release()
一個原始的鎖有兩種狀態,鎖定與解鎖,分別對應acquire()和release() 方法。
range()
range()函數來創建列表包含算術級數。
range(len(loops))理解:
>>> aa= "hello" #長度計算 >>> len(aa) 5 #創建列表 >>> range(len(aa)) [0, 1, 2, 3, 4] #循環輸出列表元素 >>> for a in range(len(aa)): print a 0 1 2 3 4
我們先調用 thread.allocate_lock()函數創建一個鎖的列表,並分別調用各個鎖的 acquire()函數獲得鎖。獲得鎖表示“把鎖鎖上”。鎖上后,我們就把鎖放到鎖列表 locks 中。
下一個循環創建線程,每個線程都用各自的循環號,睡眠時間和鎖為參數去調用 loop()函數。為什么我們不在創建鎖的循環里創建線程呢?有以下幾個原因:(1) 我們想到實現線程的同步,所以要讓“所有的馬同時沖出柵欄”。(2) 獲取鎖要花一些時間,如果你的線程退出得“太快”,可能會導致還沒有獲得鎖,線程就已經結束了的情況。
在線程結束的時候,線程要自己去做解鎖操作。最后一個循環只是坐在那一直等(達到暫停主線程的目的),直到兩個鎖都被解鎖為止才繼續運行。
mtsleep2.py運行結果:
starting at: Mon Dec 23 20:57:26 2013 start loop start loop0 1at: at:Mon Dec 23 20:57:26 2013 Mon Dec 23 20:57:26 2013 loop 1 done at: Mon Dec 23 20:57:28 2013 loop 0 done at: Mon Dec 23 20:57:30 2013 all end: Mon Dec 23 20:57:30 2013
7.2.1、threading模塊
我們應該避免使用thread模塊,原因是它不支持守護線程。當主線程退出時,所有的子線程不論它們是否還在工作,都會被強行退出。有時我們並不期望這種行為,這時就引入了守護線程的概念。threading模塊則支持守護線程。
mtsleep3.py
#coding=utf-8 import threading from time import sleep, ctime loops = [4,2] def loop(nloop, nsec): print 'start loop', nloop, 'at:', ctime() sleep(nsec) print 'loop', nloop, 'done at:', ctime() def main(): print 'starting at:', ctime() threads = [] nloops = range(len(loops)) #創建線程 for i in nloops: t = threading.Thread(target=loop,args=(i,loops[i])) threads.append(t) #開始線程 for i in nloops: threads[i].start() #等待所有結束線程 for i in nloops: threads[i].join() print 'all end:', ctime() if __name__ == '__main__': main()
運行結果:
starting at: Mon Dec 23 22:58:55 2013 start loop 0 at: Mon Dec 23 22:58:55 2013 start loop 1 at: Mon Dec 23 22:58:55 2013 loop 1 done at: Mon Dec 23 22:58:57 2013 loop 0 done at: Mon Dec 23 22:58:59 2013 all end: Mon Dec 23 22:58:59 2013
start()
開始線程活動
join()
等待線程終止
所有的線程都創建了之后,再一起調用 start()函數啟動,而不是創建一個啟動一個。而且,不用再管理一堆鎖(分配鎖,獲得鎖,釋放鎖,檢查鎖的狀態等),只要簡單地對每個線程調用 join()函數就可以了。
join()會等到線程結束,或者在給了 timeout 參數的時候,等到超時為止。join()的另一個比較重要的方面是它可以完全不用調用。一旦線程啟動后,就會一直運行,直到線程的函數結束,退出為止。
使用可調用的類
mtsleep4.py
#coding=utf-8 import threading from time import sleep, ctime loops = [4,2] class ThreadFunc(object): def __init__(self,func,args,name=''): self.name=name self.func=func self.args=args def __call__(self): apply(self.func,self.args) def loop(nloop,nsec): print "seart loop",nloop,'at:',ctime() sleep(nsec) print 'loop',nloop,'done at:',ctime() def main(): print 'starting at:',ctime() threads=[] nloops = range(len(loops)) for i in nloops: #調用ThreadFunc實例化的對象,創建所有線程 t = threading.Thread( target=ThreadFunc(loop,(i,loops[i]),loop.__name__)) threads.append(t) #開始線程 for i in nloops: threads[i].start() #等待所有結束線程 for i in nloops: threads[i].join() print 'all end:', ctime() if __name__ == '__main__': main()
運行結果:
starting at: Tue Dec 24 16:39:16 2013 seart loop 0 at: Tue Dec 24 16:39:16 2013 seart loop 1 at: Tue Dec 24 16:39:16 2013 loop 1 done at: Tue Dec 24 16:39:18 2013 loop 0 done at: Tue Dec 24 16:39:20 2013 all end: Tue Dec 24 16:39:20 2013
創建新線程的時候,Thread 對象會調用我們的ThreadFunc 對象,這時會用到一個特殊函數__call__()。由於我們已經有了要用的參數,所以就不用再傳到 Thread()的構造函數中。由於我們有一個參數的元組,這時要在代碼中使用 apply()函數。
我們傳了一個可調用的類(的實例),而不是僅傳一個函數。
__init__()
方法在類的一個對象被建立時運行。這個方法可以用來對你的對象做一些初始化。
apply()
apply(func [, args [, kwargs ]]) 函數用於當函數參數已經存在於一個元組或字典中時,間接地調用函數。args是一個包含將要提供給函數的按位置傳遞的參數的元組。如果省略了args,任何參數都不會被傳遞,kwargs是一個包含關鍵字參數的字典。
apply() 用法:
#不帶參數的方法 >>> def say(): print 'say in' >>> apply(say) say in #函數只帶元組的參數 >>> def say(a,b): print a,b >>> apply(say,('hello','蟲師')) hello 蟲師 #函數帶關鍵字參數 >>> def say(a=1,b=2): print a,b >>> def haha(**kw): apply(say,(),kw) >>> haha(a='a',b='b') a b