前言
①多線程類似於同時執行多個不同程序,多線程運行有如下優點:
- 使用線程可以把占據長時間的程序中的任務放到后台去處理。
- 用戶界面可以更加吸引人,比如用戶點擊了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度。
- 程序的運行速度可能加快。
- 在一些等待的任務實現上如用戶輸入、文件讀寫和網絡收發數據等,線程就比較有用了。在這種情況下我們可以釋放一些珍貴的資源如內存占用等等。(在用戶等待程序響應結束的同時,后台同時在執行釋放內存占用的任務)
②每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
③每個線程都有他自己的一組CPU寄存器,稱為線程的上下文,該上下文反映了線程上次運行該線程的CPU寄存器的狀態。
④指令指針和堆棧指針寄存器是線程上下文中兩個最重要的寄存器,線程總是在進程得到上下文中運行的,這些地址都用於標志擁有線程的進程地址空間中的內存。
- 線程可以被搶占(中斷)。
- 在其他線程正在運行時,線程可以暫時擱置(也稱為睡眠) -- 這就是線程的退讓。
⑤線程可以分為:
- 內核線程:由操作系統內核創建和撤銷。
- 用戶線程:不需要內核支持而在用戶程序中實現的線程。
⑥python3 線程中常用的兩個模塊為:
- _thread
- threading (推薦使用)
【注意】
thread 模塊已被廢棄。用戶可以使用 threading 模塊代替。所以,在 Python3 中不能再使用 thread 模塊。為了兼容性,Python3 將 thread 重命名為 _thread
線程和進程
進程和線程簡單舉例:
①進程:對於操作系統來說,一個任務就是一個進程(Process),比如打開一個瀏覽器就是啟動一個瀏覽器進程.
②線程:有些進程還不止同時干一件事,比如Word,它可以同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時干多件事,就需要同時運行多個《子任務》,我們把進程內的這些《子任務》稱為線程(Thread)。
線程與進程的區別:
①簡而言之,一個程序至少有一個進程;一個進程至少有一個線程。
②進程就是一個應用程序在處理機上的一次執行過程,它是一個動態的概念,而線程是進程中的一部分,進程包含多個線程在運行。
③多線程可以共享全局變量,多進程不能。多線程中,所有子線程的進程號相同;多進程中,不同的子進程進程號不同。
怎樣理解線程:
線程可以是一個python程序,也可以是python文件里的一個函數。如果python程序或者函數開始執行了,你也可以說線程開始執行了。
並行和並發:
並行處理:是計算機系統中能同時執行兩個或更多個處理的一種計算方法。並行處理可同時工作於同一程序的不同方面。並行處理的主要目的是節省大型和復雜問題的解決時間。
【①同個CPU,同一時間,執行多個線程;②多個CPU,同一時間,執行多個進程(進程與CPU一一對應)】
並發處理:指一個時間段內有幾個程序都處於已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機(CPU)上運行,但是任意一個時刻點上只有一個程序在處理機(CPU)上運行。
【同個cpu,同一時間間隔(時間窗),執行多個線程】
總結:
python的多線程原理是並發
python多線程:在python中,同個CPU里同一時間點只能運行一個線程;為了數據安全,引入全局解釋鎖(GIL),相當於一張通行證;沒有GIL的線程不進入CPU運行。
python多線程原理:一個程序運行,其他的程序不運行;當運行的線程需要等待的時候(如網絡,IO等),該線程被掛起【通行證(GIL)被拿走】等待,其他線程競爭GIL;先拿到的GIL的線程先運行。
【掛起、競爭GIL的操作是由系統調度,不需要我們管;當線程掛起、競爭、另一個線程開始執行,這個過程會消耗一點時間,稱為切換時間】
【若切換時間<等待時間,則多線程提高了效率;若切換時間與等待時間差不多,就沒有什么必要使用python多線程了】
同步與異步
同步:指一個進程在執行某個請求的時候,若該請求需要一段時間才能返回信息,那么這個進程將會一直等待下去,直到收到返回信息才繼續執行下去。
異步:指進程不需要一直等待下去,而是繼續執行下面的操作,不管其他進程的狀態,當有消息返回時系統會通知進程進行處理,這樣可以提高執行效率。
主進程與子進程:
Threading.Thread封裝的函數 == 子進程 ; 其他部分 == 主進程
子線程其實就是並發(同一時間段內執行多個操作)的任務;
這些任務一般都會寫進一個函數func或者類里面,然后兩步走,如下代碼示例:
thread = Threading.Thread(target=函數func, args=(參數1,參數2)) #用Thread類包(封裝)起來
thread.start() #start之后就開始跑了
setDaemon(Ture) :設置子進程為守護進程 == 主進程關閉,子進程隨即關閉【當你覺得一些線程不重要的時候,可以設置守護線程。】
Join() :設置阻塞 == 該子進程執行完才能執行主線程【當一些任務要先於另一些任務完成的時候,可以用】
對於非守護線程,就算主線程執行完了,也要等非守護進程完成才能退出
單線程:
在好些年前的MS-DOS時代,操作系統處理問題都是單任務的。比如我想做聽音樂和看電影兩件事兒,那么一定要先排一下順序。
from time import ctime,sleep def music(): for i in range(2): print "I was listening to music. %s" %ctime() sleep(1) def movie(): for i in range(2): print "I was at the movies! %s" %ctime() sleep(5) if __name__ == '__main__': music() movie() print "all over %s" %ctime()
我們先聽了一首音樂,通過for循環來控制音樂的播放了兩次,每首音樂播放需要1秒鍾,sleep()來控制音樂播放的時長。接着我們又看了一場電影。
每一場電影需要5秒鍾,因為太好看了,所以我也通過for循環看兩遍。在整個休閑娛樂活動結束后,我通過 print "all over %s" %ctime() 看了一下當前時間,差不多該睡覺了。
運行結果:
>>=========================== RESTART ================================ >>> I was listening to music. Thu Apr 17 10:47:08 2014 I was listening to music. Thu Apr 17 10:47:09 2014 I was at the movies! Thu Apr 17 10:47:10 2014 I was at the movies! Thu Apr 17 10:47:15 2014 all over Thu Apr 17 10:47:20 2014
其實,music()函數和move()函數更應該被看作是音樂和視頻播放器,至於要播放什么歌曲和視頻應該由我們使用時決定。
所以對上述代碼進行適當改造:
#coding=utf-8 import threading from time import ctime,sleep def music(func): for i in range(2): print "I was listening to %s. %s" %(func,ctime()) sleep(1) def move(func): for i in range(2): print "I was at the %s! %s" %(func,ctime()) sleep(5) if __name__ == '__main__': music(u'愛情買賣') move(u'阿凡達') print "all over %s" %ctime()
運行結果:
>>> ======================== RESTART ================================ >>> I was listening to 愛情買賣. Thu Apr 17 11:48:59 2014 I was listening to 愛情買賣. Thu Apr 17 11:49:00 2014 I was at the 阿凡達! Thu Apr 17 11:49:01 2014 I was at the 阿凡達! Thu Apr 17 11:49:06 2014 all over Thu Apr 17 11:49:11 2014
多線程1:
Python中使用線程有兩種方式:函數或者用類來包裝線程對象。
1、函數式:調用 _thread 模塊中的 start_new_thread() 函數來產生新線程。語法如下:
_thread.start_new_thread (function, args[, kwargs])
參數說明:
- function - 線程函數。
- args - 傳遞給線程函數的參數,必須是個tuple類型。
- kwargs - 可選參數。
實例:
# -*- encoding:utf-8 -*- import _thread import time # 為線程定義一個函數 def print_time(threadName, delay): count = 0 while count < 5: time.sleep(delay) count += 1 print("%s: %s" % (threadName, time.ctime(time.time()))) # 創建兩個線程 try: _thread.start_new_thread(print_time, ("Thread-1", 2,)) _thread.start_new_thread(print_time, ("Thread-2", 4,)) except: print("Error: 無法啟動線程") while 1: pass
運行結果:執行以上程后可以按下 ctrl-c 退出。
threading 線程模塊
①Python3 通過兩個標准庫 _thread 和 threading 提供對線程的支持。
②_thread 提供了低級別的、原始的線程以及一個簡單的鎖,它相比於 threading 模塊的功能還是比較有限的。
③ threading 模塊除了包含 _thread 模塊中的所有方法外,還提供的其他方法:
- threading.currentThread() :返回當前的線程變量。
- threading.enumerate() : 返回一個包含正在運行的線程的列表list。正在運行指線程啟動后、結束前,不包括啟動前和終止后的線程。
- threading.activeCount() :返回正在運行的線程數量,與 len(threading.enumerate()) 有相同的結果。
除了使用方法外,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:
- run() :用以表示線程活動的方法。
- start() :啟動線程活動。
- join([time]) :等待至線程中止。這阻塞調用線程直至線程的join() 方法被調用中止-正常退出或者拋出未處理的異常-或者是可選的超時發生。
- isAlive() :返回線程是否活動的。
- getName() :返回線程名。
- setName() :設置線程名。
使用 threading 線程模塊創建線程
我們可以通過直接從 threading.Thread 繼承創建一個新的子類,並實例化后調用 start() 方法啟動新線程;即它調用了threading線程模塊中的Thread類中的 start() 方法:
import threading import time exitFlag = 0 class myThread(threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print("開始線程:" + self.name) print_time(self.name, self.counter, 5) print("退出線程:" + self.name) def print_time(threadName, delay, counter): while counter: if exitFlag: threadName.exit() time.sleep(delay) print("%s: %s" % (threadName, time.ctime(time.time()))) counter -= 1 # 創建新線程 thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # 開啟新線程 thread1.start() thread2.start() thread1.join() thread2.join() print("退出主線程")
運行結果:
線程同步
①如果多個線程共同對某個數據修改,則可能出現不可預料的結果,為了保證數據的正確性,需要對多個線程進行同步。
②使用 Thread 對象的 Lock 和 Rlock 可以實現簡單的線程同步,這兩個對象都有 acquire 方法和 release 方法,對於那些需要每次只允許一個線程操作的數據,可以將其操作放到 acquire 和 release 方法之間。如下:
③多線程的優勢在於可以同時運行多個任務(至少感覺起來是這樣)。但是當線程需要共享數據時,可能存在數據不同步的問題。
考慮這樣一種情況:一個列表里所有元素都是0,線程"set"從后向前把所有元素改成1,而線程"print"負責從前往后讀取列表並打印。
那么,可能線程"set"開始改的時候,線程"print"便來打印列表了,輸出就成了一半0一半1,這就是數據的不同步。為了避免這種情況,引入了鎖的概念。
④鎖有兩種狀態——鎖定和未鎖定。每當一個線程比如"set"要訪問共享數據時,必須先獲得鎖定;如果已經有別的線程比如"print"獲得鎖定了,那么就讓線程"set"暫停,也就是同步阻塞;等到線程"print"訪問完畢,釋放鎖以后,再讓線程"set"繼續。
經過這樣的處理,打印列表時要么全部輸出0,要么全部輸出1,不會再出現一半0一半1的尷尬場面。
實例:
import threading import time class myThread (threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print ("開啟線程: " + self.name) # 獲取鎖,用於線程同步 threadLock.acquire() print_time(self.name, self.counter, 3) # 釋放鎖,開啟下一個線程 threadLock.release() def print_time(threadName, delay, counter): while counter: time.sleep(delay) print ("%s: %s" % (threadName, time.ctime(time.time()))) counter -= 1 threadLock = threading.Lock() threads = [] # 創建新線程 thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # 開啟新線程 thread1.start() thread2.start() # 添加線程到線程列表 threads.append(thread1) threads.append(thread2) # 等待所有線程完成 for t in threads: t.join() print ("退出主線程")
運行結果:
線程優先級隊列( Queue)
Python 的 Queue 模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(后入先出)隊列LifoQueue,和優先級隊列 PriorityQueue。
這些隊列都實現了鎖原語,能夠在多線程中直接使用,可以使用隊列來實現線程間的同步。
Queue 模塊中的常用方法:
- Queue.qsize():返回隊列的大小
- Queue.empty():如果隊列為空,返回True,反之False
- Queue.full():如果隊列滿了,返回True,反之False
- Queue.full 與 maxsize 大小對應
- Queue.get([block[, timeout]]):獲取隊列,timeout等待時間
- Queue.get_nowait():相當Queue.get(False)
- Queue.put(item):寫入隊列,timeout等待時間
- Queue.put_nowait(item):相當Queue.put(item, False)
- Queue.task_done():在完成一項工作之后,Queue.task_done()函數向任務已經完成的隊列發送一個信號
- Queue.join():實際上意味着等到隊列為空,再執行別的操作
實例:
import queue import threading import time exitFlag = 0 class myThread (threading.Thread): def __init__(self, threadID, name, q): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.q = q def run(self): print ("開啟線程:" + self.name) process_data(self.name, self.q) print ("退出線程:" + self.name) def process_data(threadName, q): while not exitFlag: queueLock.acquire() if not workQueue.empty(): data = q.get() queueLock.release() print ("%s processing %s" % (threadName, data)) else: queueLock.release() time.sleep(1) threadList = ["Thread-1", "Thread-2", "Thread-3"] nameList = ["One", "Two", "Three", "Four", "Five"] queueLock = threading.Lock() workQueue = queue.Queue(10) threads = [] threadID = 1 # 創建新線程 for tName in threadList: thread = myThread(threadID, tName, workQueue) thread.start() threads.append(thread) threadID += 1 # 填充隊列 queueLock.acquire() for word in nameList: workQueue.put(word) queueLock.release() # 等待隊列清空 while not workQueue.empty(): pass # 通知線程是時候退出 exitFlag = 1 # 等待所有線程完成 for t in threads: t.join() print ("退出主線程")
運行結果:
多線程2
科技在發展,時代在進步,我們的CPU也越來越快,CPU抱怨,P大點事兒占了我一定的時間,其實我同時干多個活都沒問題的;於是,操作系統就進入了多任務時代。我們聽着音樂吃着火鍋的不在是夢想。
繼續對上面的例子進行改造,引入threadring來同時播放音樂和視頻:
#coding=utf-8 import threading from time import ctime,sleep def music(func): for i in range(2): print "I was listening to %s. %s" %(func,ctime()) sleep(1) def move(func): for i in range(2): print "I was at the %s! %s" %(func,ctime()) sleep(5) threads = [] t1 = threading.Thread(target=music,args=(u'愛情買賣',)) threads.append(t1) t2 = threading.Thread(target=move,args=(u'阿凡達',)) threads.append(t2) if __name__ == '__main__': for t in threads: t.setDaemon(True) t.start() print "all over %s" %ctime()
運行結果:
>>> ========================= RESTART ================================
>>>
I was listening to 愛情買賣. Thu Apr 17 12:51:45 2014
I was at the 阿凡達! Thu Apr 17 12:51:45 2014
all over Thu Apr 17 12:51:45 2014
從執行結果來看,子線程(muisc() 、movie() )和主線程(print "all over %s" %ctime())都是同一時間啟動,但由於主線程執行完結束,所以導致子線程也終止。
繼續調整程序:
... if __name__ == '__main__': for t in threads: t.setDaemon(True) t.start() for t in threads: t.join() print "all over %s" %ctime()
我們只對上面的程序加了個join()方法,用於等待線程終止。
join()的作用是,在子線程完成運行之前,這個子線程的父線程將一直被阻塞。
注意: join()方法的位置是在for循環外的,也就是說必須等待for循環里的兩個進程都結束后,才去執行主進程。
運行結果:
>>> ========================= RESTART ================================ >>> I was listening to 愛情買賣. Thu Apr 17 13:04:11 2014 I was at the 阿凡達! Thu Apr 17 13:04:11 2014 I was listening to 愛情買賣. Thu Apr 17 13:04:12 2014 I was at the 阿凡達! Thu Apr 17 13:04:16 2014 all over Thu Apr 17 13:04:21 2014
從執行結果可看到,music() 和movie() 是同時啟動的。
開始時間4分11秒,直到調用主進程為4分22秒,總耗時為10秒。從單線程時減少了2秒,我們可以把music的sleep()的時間調整為4秒。
... def music(func): for i in range(2): print "I was listening to %s. %s" %(func,ctime()) sleep(4) ...
執行結果:
>>> ====================== RESTART ================================ >>> I was listening to 愛情買賣. Thu Apr 17 13:11:27 2014I was at the 阿凡達! Thu Apr 17 13:11:27 2014 I was listening to 愛情買賣. Thu Apr 17 13:11:31 2014 I was at the 阿凡達! Thu Apr 17 13:11:32 2014 all over Thu Apr 17 13:11:37 2014
子線程啟動11分27秒,主線程運行11分37秒。
雖然music每首歌曲從1秒延長到了4 ,但通多程線的方式運行腳本,總的時間沒變化。
詳細解釋:
import threading #首先導入threading 模塊,這是使用多線程的前提。 threads = [] t1 = threading.Thread(target=music,args=(u'愛情買賣',)) #創建了threads數組,創建線程t1,使用threading.Thread()方法,在這個方法中調用music方法target=music,args方法對music進行傳參。 把創建好的線程t1裝到threads數組中。 threads.append(t1) #接着以同樣的方式創建線程t2,並把t2也裝到threads數組。 t2 = threading.Thread(target=move,args=(u'阿凡達',)) threads.append(t2) #最后通過for循環遍歷數組。(數組被裝載了t1和t2兩個線程) for t in threads: t.setDaemon(True) t.start() #setDaemon(True)將線程聲明為守護線程,必須在start() 方法調用之前設置,如果不設置為守護線程程序會被無限掛起。子線程啟動后,父線程也繼續執行下去,當父線程執行完最后一條語句print "all over %s" %ctime()后,沒有等待子線程,直接就退出了,同時子線程也一同結束。 setDaemon() #開始線程活動。 start()