1. Python 多線程
多線程類似於同時執行多個不同程序,多線程運行有如下優點:
- 使用線程可以把佔據長時間的程序中的任務放到後臺去處理。
- 用戶界面可以更加吸引人,這樣比如用戶點擊了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度
- 程序的運行速度可能加快
- 在一些等待的任務實現上如用戶輸入、文件讀寫和網絡收發數據等,線程就比較有用了。在這種情況下我們可以釋放一些珍貴的資源如內存佔用等等。
線程在執行過程中與進程還是有區別的。每個獨立的進程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
每個線程都有他自己的一組CPU寄存器,稱爲線程的上下文,該上下文反映了線程上次運行該線程的CPU寄存器的狀態。
指令指針和堆棧指針寄存器是線程上下文中兩個最重要的寄存器,線程總是在進程得到上下文中運行的,這些地址都用於標誌擁有線程的進程地址空間中的內存。
- 線程可以被搶佔(中斷)。
- 在其他線程正在運行時,線程可以暫時擱置(也稱爲睡眠) -- 這就是線程的退讓。
開始學習Python線程
Python中使用線程有兩種方式:函數或者用類來包裝線程對象。
函數式:調用thread模塊中的start_new_thread()函數來產生新線程。語法如下:
thread.start_new_thread ( function, args[, kwargs] )
參數說明:
- function - 線程函數。
- args - 傳遞給線程函數的參數,他必須是個tuple類型。
- kwargs - 可選參數。
實例(Python 2.0+)
#!/usr/bin/python # -*- coding: 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: unable to start thread" while 1: pass
執行以上程序輸出結果如下:
Thread-1: Thu Jan 22 15:42:17 2009
Thread-1: Thu Jan 22 15:42:19 2009
Thread-2: Thu Jan 22 15:42:19 2009
Thread-1: Thu Jan 22 15:42:21 2009
Thread-2: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:25 2009
Thread-2: Thu Jan 22 15:42:27 2009
Thread-2: Thu Jan 22 15:42:31 2009
Thread-2: Thu Jan 22 15:42:35 2009
線程的結束一般依靠線程函數的自然結束;也可以在線程函數中調用thread.exit(),他拋出SystemExit exception,達到退出線程的目的。
線程模塊
Python通過兩個標準庫thread和threading提供對線程的支持。thread提供了低級別的、原始的線程以及一個簡單的鎖。
threading 模塊提供的其他方法:
- threading.currentThread(): 返回當前的線程變量。
- threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啓動後、結束前,不包括啓動前和終止後的線程。
- threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果。
除了使用方法外,線程模塊同樣提供了Thread類來處理線程,Thread類提供了以下方法:
- run(): 用以表示線程活動的方法。
- start():啓動線程活動。
- join([time]): 等待至線程中止。這阻塞調用線程直至線程的join() 方法被調用中止-正常退出或者拋出未處理的異常-或者是可選的超時發生。
- isAlive(): 返回線程是否活動的。
- getName(): 返回線程名。
- setName(): 設置線程名。
使用Threading模塊創建線程
使用Threading模塊創建線程,直接從threading.Thread繼承,然後重寫__init__方法和run方法:
實例(Python 2.0+)
#!/usr/bin/python # -*- coding: UTF-8 -*- import threading import time exitFlag = 0 class myThread (threading.Thread): #繼承父類threading.Thread def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): #把要執行的代碼寫到run函數裏面 線程在創建後會直接運行run函數 print "Starting " + self.name print_time(self.name, self.counter, 5) print "Exiting " + self.name def print_time(threadName, delay, counter): while counter: if exitFlag: (threading.Thread).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() print "Exiting Main Thread"
以上程序執行結果如下;
Starting Thread-1
Starting Thread-2
Exiting Main Thread
Thread-1: Thu Mar 21 09:10:03 2013
Thread-1: Thu Mar 21 09:10:04 2013
Thread-2: Thu Mar 21 09:10:04 2013
Thread-1: Thu Mar 21 09:10:05 2013
Thread-1: Thu Mar 21 09:10:06 2013
Thread-2: Thu Mar 21 09:10:06 2013
Thread-1: Thu Mar 21 09:10:07 2013
Exiting Thread-1
Thread-2: Thu Mar 21 09:10:08 2013
Thread-2: Thu Mar 21 09:10:10 2013
Thread-2: Thu Mar 21 09:10:12 2013
Exiting Thread-2
2. 線程同步機制: Locks, RLocks, Semaphores, Conditions, Events
本文詳細地闡述了Python線程同步機制。你將學習到以下有關Python線程同步機制:Lock,RLock,Semaphore,Condition,Event和Queue,還有Python的內部是如何實現這些機制的。 本文給出的程序的源代碼可以在github上找到。
首先讓我們來看一個沒有使用線程同步的簡單程序。
線程(Threading)
我們希望編程一個從一些URL中獲得內容並且將內容寫入文件的程序,完成這個程序可以不使用線程,爲了加快獲取的速度,我們使用2個線程,每個線程處理一半的URL。
注:完成這個程序的最好方式是使用一個URL隊列,但是以下面的例子開始我的講解更加合適。
類FetchUrls是threading.Thread的子類,他擁有一個URL列表和一個寫URL內容的文件對象。
|
main函數啓動了兩個線程,之後讓他們下載URL內容。
|
上面的程序將出現兩個線程同時寫一個文件的情況,導致文件一團亂碼。我們需要找到一種在給定的時間裏只有一個線程寫文件的方法。實現的方法就是使用像鎖(Locks)這樣的線程同步機制。
鎖(Lock)
鎖有兩種狀態:被鎖(locked)和沒有被鎖(unlocked)。擁有acquire()和release()兩種方法,並且遵循一下的規則:
- 如果一個鎖的狀態是unlocked,調用acquire()方法改變它的狀態爲locked;
- 如果一個鎖的狀態是locked,acquire()方法將會阻塞,直到另一個線程調用release()方法釋放了鎖;
- 如果一個鎖的狀態是unlocked調用release()會拋出RuntimeError異常;
- 如果一個鎖的狀態是locked,調用release()方法改變它的狀態爲unlocked。
解決上面兩個線程同時寫一個文件的問題的方法就是:我們給類FetchUrls的構造器中傳入一個鎖(lock),使用這個鎖來保護文件操作,實現在給定的時間只有一個線程寫文件。下面的代碼只顯示了關於lock部分的修改。完整的源碼可以在threads/lock.py中找到。
|
以下是程序的輸出:
|
從上面的輸出我們可以看出,寫文件的操作被鎖保護,沒有出現兩個線程同時寫一個文件的現象。
下面我們看一下Python內部是如何實現鎖(Lock)的。我正在使用的Python版本是Linux操作系統上的Python 2.6.6。
threading模塊的Lock()方法就是thread.allocate_lock,代碼可以在Lib/threading.py中找到。
|
C的實現在Python/thread_pthread.h中。程序假定你的系統支持POSIX信號量(semaphores)。sem_init()初始化鎖(Lock)所在地址的信號量。初始的信號量值是1,意味着鎖沒有被鎖(unlocked)。信號量將在處理器的不同線程之間共享。
|
當acquire()方法被調用時,下面的C代碼將被執行。默認的waitflag值是1,表示調用將被被阻塞直到鎖被釋放。sem_wait()方法減少信號量的值或者被阻塞直到信號量大於零。
|
當release()方法被調用時,下面的C代碼將被執行。sem_post()方法增加信號量。
|
可以將鎖(Lock)與“with”語句一起使用,鎖可以作爲上下文管理器(context manager)。使用“with”語句的好處是:當程序執行到“with”語句時,acquire()方法將被調用,當程序執行完“with”語句時,release()方法會被調用(譯註:這樣我們就不用顯示地調用acquire()和release()方法,而是由“with”語句根據上下文來管理鎖的獲取和釋放。)下面我們用“with”語句重寫FetchUrls類。
|
可重入鎖(RLock)
RLock是可重入鎖(reentrant lock),acquire()能夠不被阻塞的被同一個線程調用多次。要注意的是release()需要調用與acquire()相同的次數才能釋放鎖。
使用Lock,下面的代碼第二次調用acquire()時將被阻塞:
|
如果你使用的是RLock,下面的代碼第二次調用acquire()不會被阻塞:
|
RLock使用的同樣是thread.allocate_lock(),不同的是他跟蹤宿主線程(the owner thread)來實現可重入的特性。下面是RLock的acquire()實現。如果調用acquire()的線程是資源的所有者,記錄調用acquire()次數的計數器就會加1。如果不是,就將試圖去獲取鎖。線程第一次獲得鎖時,鎖的擁有者將會被保存,同時計數器初始化爲1。
|
下面我們看一下可重入鎖(RLock)的release()方法。首先它會去確認調用者是否是鎖的擁有者。如果是的話,計數器減1;如果計數器爲0,那麼鎖將會被釋放,這時其他線程就可以去獲取鎖了。
|
條件(Condition)
條件同步機制是指:一個線程等待特定條件,而另一個線程發出特定條件滿足的信號。 解釋條件同步機制的一個很好的例子就是生產者/消費者(producer/consumer)模型。生產者隨機的往列表中“生產”一個隨機整數,而消費者從列表中“消費”整數。完整的源碼可以在threads/condition.py中找到
在producer類中,producer獲得鎖,生產一個隨機整數,通知消費者有了可用的“商品”,並且釋放鎖。producer無限地向列表中添加整數,同時在兩個添加操作中間隨機的停頓一會兒。
|
下面是消費者(consumer)類。它獲取鎖,檢查列表中是否有整數,如果沒有,等待生產者的通知。當消費者獲取整數之後,釋放鎖。
注意在wait()方法中會釋放鎖,這樣生產者就能獲得資源並且生產“商品”。
|
下面我們編寫main方法,創建兩個線程:
|
下面是程序的輸出:
|
Thread-1添加159到列表中,通知消費者同時釋放鎖,Thread-2獲得鎖,取回159,並且釋放鎖。此時因爲執行time.sleep(1),生產者正在睡眠,當消費者再次試圖獲取整數時,列表中並沒有整數,這時消費者進入等待狀態,等待生產者的通知。當wait()被調用時,它會釋放資源,從而生產者能夠利用資源生產整數。
下面我們看一下Python內部是如何實現條件同步機制的。如果用戶沒有傳入鎖(lock)對象,condition類的構造器創建一個可重入鎖(RLock),這個鎖將會在調用acquire()和release()時使用。
|
接下來是wait()方法。爲了簡化說明,我們假定在調用wait()方法時不使用timeout參數。wait()方法創建了一個名爲waiter的鎖,並且設置鎖的狀態爲locked。這個waiter鎖用於線程間的通訊,這樣生產者(在生產完整數之後)就可以通知消費者釋放waiter()鎖。鎖對象將會被添加到等待者列表,並且在調用waiter.acquire()時被阻塞。一開始condition鎖的狀態被保存,並且在wait()結束時被恢復。
|
當生產者調用notify()方法時,notify()釋放waiter鎖,喚醒被阻塞的消費者。
|
同樣Condition對象也可以和“with”語句一起使用,這樣“with”語句上下文會幫我們調用acquire()和release()方法。下面的代碼使用“with”語句改寫了生產者和消費者類。
|
信號量(Semaphore)
信號量同步基於內部計數器,每調用一次acquire(),計數器減1;每調用一次release(),計數器加1.當計數器爲0時,acquire()調用被阻塞。這是迪科斯徹(Dijkstra)信號量概念P()和V()的Python實現。信號量同步機制適用於訪問像服務器這樣的有限資源。
信號量同步的例子:
|
讓我們看一下信號量同步在Python內部是如何實現的。構造器使用參數value來表示計數器的初始值,默認值爲1。一個條件鎖實例用於保護計數器,同時當信號量被釋放時通知其他線程。
|
acquire()方法。如果信號量爲0,線程被條件鎖的wait()方法阻塞,直到被其他線程喚醒;如果計數器大於0,調用acquire()使計數器減1。
|
信號量類的release()方法增加計數器的值並且喚醒其他線程。
|
還有一個“有限”(bounded)信號量類,可以確保release()方法的調用次數不能超過給定的初始信號量數值(value參數),下面是“有限”信號量類的Python代碼:
|
同樣信號量(Semaphore)對象可以和“with”一起使用:
|
事件(Event)
基於事件的同步是指:一個線程發送/傳遞事件,另外的線程等待事件的觸發。 讓我們再來看看前面的生產者和消費者的例子,現在我們把它轉換成使用事件同步而不是條件同步。完整的源碼可以在threads/event.py裏面找到。
首先是生產者類,我們傳入一個Event實例給構造器而不是Condition實例。一旦整數被添加進列表,事件(event)被設置和發送去喚醒消費者。注意事件(event)實例默認是被髮送的。
|
同樣我們傳入一個Event實例給消費者的構造器,消費者阻塞在wait()方法,等待事件被觸發,即有可供消費的整數。
|
下面是程序的輸出,Thread-1添加124到整數列表中,然後設置事件並且喚醒消費者。消費者從wait()方法中喚醒,在列表中獲取到整數。
|
事件鎖的Python內部實現,首先是Event鎖的構造器。構造器中創建了一個條件(Condition)鎖,來保護事件標誌(event flag),同事喚醒其他線程當事件被設置時。
|
接下來是set()方法,它設置事件標誌爲True,並且喚醒其他線程。條件鎖對象保護程序修改事件標誌狀態的關鍵部分。
|
而clear()方法正好相反,它設置時間標誌爲False。
|
最後,wait()方法將阻塞直到調用了set()方法,當事件標誌爲True時,wait()方法就什麼也不做。
|
Queue
Queue是python標準庫中的線程安全的隊列(FIFO)實現,提供了一個適用於多線程編程的先進先出的數據結構,即隊列,用來在生產者和消費者線程之間的信息傳遞
基本FIFO隊列
class Queue.Queue(maxsize=0)
FIFO即First in First Out,先進先出。Queue提供了一個基本的FIFO容器,使用方法很簡單,maxsize是個整數,指明瞭隊列中能存放的數據個數的上限。一旦達到上限,插入會導致阻塞,直到隊列中的數據被消費掉。如果maxsize小於或者等於0,隊列大小沒有限制。
舉個栗子:
import Queue
q = Queue.Queue()
for i in range(5):
q.put(i)
while not q.empty():
print q.get()
輸出:
0
1
2
3
4
LIFO隊列
class Queue.LifoQueue(maxsize=0)
LIFO即Last in First Out,後進先出。與棧的類似,使用也很簡單,maxsize用法同上
再舉個栗子:
import Queue
q = Queue.LifoQueue()
for i in range(5):
q.put(i)
while not q.empty():
print q.get()
輸出:
4
3
2
1
0
可以看到僅僅是將Queue.Quenu類
替換爲Queue.LifiQueue類
優先級隊列
class Queue.PriorityQueue(maxsize=0)
構造一個優先隊列。maxsize用法同上。
import Queue
import threading
class Job(object):
def __init__(self, priority, description):
self.priority = priority
self.description = description
print 'Job:',description
return
def __cmp__(self, other):
return cmp(self.priority, other.priority)
q = Queue.PriorityQueue()
q.put(Job(3, 'level 3 job'))
q.put(Job(10, 'level 10 job'))
q.put(Job(1, 'level 1 job'))
def process_job(q):
while True:
next_job = q.get()
print 'for:', next_job.description
q.task_done()
workers = [threading.Thread(target=process_job, args=(q,)),
threading.Thread(target=process_job, args=(q,))
]
for w in workers:
w.setDaemon(True)
w.start()
q.join()
結果
Job: level 3 job
Job: level 10 job
Job: level 1 job
for: level 1 job
for: level 3 job
for: job: level 10 job
一些常用方法
task_done()
意味着之前入隊的一個任務已經完成。由隊列的消費者線程調用。每一個get()調用得到一個任務,接下來的task_done()調用告訴隊列該任務已經處理完畢。
如果當前一個join()正在阻塞,它將在隊列中的所有任務都處理完時恢復執行(即每一個由put()調用入隊的任務都有一個對應的task_done()調用)。
join()
阻塞調用線程,直到隊列中的所有任務被處理掉。
只要有數據被加入隊列,未完成的任務數就會增加。當消費者線程調用task_done()(意味着有消費者取得任務並完成任務),未完成的任務數就會減少。當未完成的任務數降到0,join()解除阻塞。
put(item[, block[, timeout]])
將item放入隊列中。
- 如果可選的參數block爲True且timeout爲空對象(默認的情況,阻塞調用,無超時)。
- 如果timeout是個正整數,阻塞調用進程最多timeout秒,如果一直無空空間可用,拋出Full異常(帶超時的阻塞調用)。
- 如果block爲False,如果有空閒空間可用將數據放入隊列,否則立即拋出Full異常
其非阻塞版本爲put_nowait
等同於put(item, False)
get([block[, timeout]])
從隊列中移除並返回一個數據。block跟timeout參數同put
方法
其非阻塞方法爲`get_nowait()`相當與get(False)
empty()
如果隊列爲空,返回True,反之返回False
3. 全局解釋器鎖
全局解釋器鎖,是計算機程序設計語言解釋器用於同步線程的一種機制,它使得任何時刻僅有一個線程在執行。即便在多核心處理器上,使用 GIL 的解釋器也只允許同一時間執行一個線程。常見的使用GIL 的解釋器有CPython與Ruby MRI。
全局解釋器鎖使得python多線程成了先天殘疾,不能發揮多線程的優勢, 但是GIL會在IO訪問時釋放, 因此學習python多線程對於IO密集型的問題還是能都明顯提高效率的。