Python 執行緒同步與互斥

世界看我我看世界發表於2015-11-04

什麼是併發?

在作業系統中,指一個時間段內有幾個程式都處於已啟動到執行結束之間的狀態,並且這幾個程式都是在同一個處理機上執行的,但任一個時間點卻只有一個程式在處理機上執行。
注意併發與並行並不是同一個概念。併發是指一個時間段內同時執行,表示的是一個區間,而並行是指在同一個時間點上都在執行,是一個點,並且併發在同一時間點上只能有一個程式在執行。
在實際應用中,多個執行緒往往會共享一些資料(如:記憶體堆疊、串列埠、檔案等),並且執行緒間的狀態和行為都是互相影響的。併發執行緒的兩種關係:同步與互斥。

執行緒同步與互斥

互斥:執行緒之間通過對資源的競爭,所產生的相互制約的關係,就是互斥關係。這類執行緒間主要的問題就是互斥和死鎖的問題。

同步:程式之間不是相互排斥的關係,而是相互依賴的關係。換句話說,就是多程式共享同一臨界資源時,前一個程式輸出作為後一個程式的輸入,當第一個程式沒有輸出時,第二個程式必須等待。因為當多個執行緒共享資料時,可能會導致資料處理出錯,因此執行緒同步主要的目的就是使併發執行的各執行緒之間能夠有效的共享資源和相互合作,從而使程式的執行具有可再現性。
共享資料指的是併發執行的多個執行緒間所操作的同一資料資源。
出現共享資源訪問衝突的實質就是執行緒間沒有互斥的使用共享資源,也就是說併發執行過程中,某一個執行緒正在對共享資源訪問時,比如寫,此時其它的執行緒就不能訪問這個共享資料,直到正在訪問它的執行緒訪問結束。避免互斥,我們通過對共享資源進行加鎖操作來避免訪問衝突。當有執行緒拿到訪問這個共享資料的許可權時,就對其加一把鎖,這樣別的執行緒由於得不到訪問的鎖,所以不能訪問,直到執行緒釋放了這把鎖,其它執行緒才能訪問。
執行緒將的同步與互斥,是為了保證所共享的資料的一致性。

關於執行緒互斥的例項:

import threading
import time

data = 0
lock = threading.Lock()#建立一個鎖物件

def func() :
  global data
  print "%s acquire lock...\n" %threading.currentThread().getName()
  if lock.acquire() :
    print "%s get lock...\n" %threading.currentThread().getName()
    data += 1 #must lock
    time.sleep(2)#其它操作
    print "%s release lock...\n" %threading.currentThread().getName()

    #呼叫release()將釋放鎖
    lock.release()

startTime = time.time()
t1 = threading.Thread(target = func)
t2 = threading.Thread(target = func)
t3 = threading.Thread(target = func)
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()

endTime = time.time()
print "used time is", endTime - startTime

執行結果:

Thread-1 acquire lock...

Thread-1 get lock...
Thread-2 acquire lock...

Thread-3 acquire lock...


Thread-1 release lock...

Thread-2 get lock...

Thread-2 release lock...

Thread-3 get lock...

Thread-3 release lock...

used time is 6.0039999485

上邊的例項建立了3個執行緒t1、t2和t3同步執行,三個執行緒都訪問全域性變數data,並改變它的值。當第一個執行緒t1請求鎖成功後,開始訪問共享資料data,第二個執行緒t2和t2也開始請求鎖,但是此時t1還沒有釋放鎖,所以t2、t3處於等待鎖狀態,直到t1呼叫lock.release()釋放鎖以後,t2才得到鎖,然後執行完釋放鎖,t3才能得到鎖。這樣就保證了這三個執行緒共享資料data的一致性和同步性。並且這三個執行緒是併發執行的,沒有人為控制其獲得鎖的順序,所以它們執行的順序也是不定的。
注意:
呼叫acquire([timeout])時,執行緒將一直阻塞,直到獲得鎖定或者直到timeout秒後返回是否獲得鎖。

關於執行緒同步的一個經典例項:生產者與消費者

#coding=utf-8  
from Queue import Queue   #佇列類 
import random    
import threading    
import time      

#生成者執行緒
class Producer(threading.Thread):    
    def __init__(self, t_name, queue): 
        #呼叫父執行緒的構造方法。
        threading.Thread.__init__(self, name=t_name)    
        self.data=queue  

    def run(self):    
        for i in range(5):    
            print "%s: %s is producing %d to the queue!\n" %(time.ctime(), self.getName(), i)  
            self.data.put(i)#向佇列中新增資料    
            #產生一個0-2之間的隨機數進行睡眠
            time.sleep(random.randrange(10)/5)    
        print "%s: %s finished!" %(time.ctime(), self.getName()) 
#消費者執行緒 
class Consumer(threading.Thread):    
    def __init__(self, t_name, queue):    
        threading.Thread.__init__(self, name=t_name)  
        self.data=queue  

    def run(self):    
        for i in range(5):    
            val = self.data.get()#從佇列中取出資料    
            print "%s: %s is consuming. %d in the queue is consumed!\n" %(time.ctime(), self.getName(), val)
            time.sleep(random.randrange(10))  

        print "%s: %s finished!" %(time.ctime(), self.getName())  
#Main thread    
def main():    
    queue = Queue()#建立一個佇列物件(特點先進先出)    
    producer = Producer('Pro.', queue)#生產者物件    
    consumer = Consumer('Con.', queue)#消費者物件
    producer.start()    
    consumer.start()  
    producer.join()    
    consumer.join()  
    print 'All threads terminate!'  

if __name__ == '__main__':  
    main()

通過使用Python的佇列類,進行執行緒同步處理時,就不需要考慮加鎖的問題了,因為佇列內部會自動加鎖進行處理。

死鎖

如果程式中多個執行緒相互等待對方持有的鎖,而在得到對方的鎖之前都不釋放自己的鎖,由此導致了這些執行緒不能繼續執行,這就是死鎖。
死鎖的表現是:程式死迴圈。
防止死鎖一般的做法是:如果程式要訪問多個共享資料,則首先要從全域性考慮定義一個獲得鎖的順序,並且在整個程式中都遵守這個順序。釋放鎖時,按加鎖的反序釋放即可。
所以必須是有兩個及其以上的的併發執行緒,才能出現死鎖,如果是多於2個執行緒之間出現死鎖,那他們請求鎖的關係一定是形成了一個環,比如A等B的鎖,B等C的鎖,C等A的鎖。

例項:

#coding=utf-8
import threading  
import time

lock1 = threading.Lock()  
lock2 = threading.Lock()  
print lock1, lock2
class T1(threading.Thread):  
    def __init__(self, name):  
        threading.Thread.__init__(self)  
        self.t_name = name  

    def run(self):  
        lock1.acquire()  
        time.sleep(1)#睡眠的目的是讓執行緒2獲得排程,得到第二把鎖
        print 'in thread T1',self.t_name
        time.sleep(2) 
        lock2.acquire() #執行緒1請求第二把鎖  
        print 'in lock l2 of T1'  
        lock2.release()      
        lock1.release() 

class T2(threading.Thread):  
    def __init__(self, name):  
        threading.Thread.__init__(self)  
        self.t_name = name  

    def run(self):  
        lock2.acquire()  
        time.sleep(2)#睡眠的目的是讓執行緒1獲得排程,得到第一把鎖
        print 'in thread T2',self.t_name
        lock1.acquire() #執行緒2請求第一把鎖
        print 'in lock l1 of T2'
        lock1.release() 
        lock2.release() 

def test():  
    thread1 = T1('A')  
    thread2 = T2('B')  
    thread1.start()  
    thread2.start()  

if __name__== '__main__':  
    test()  

上面的例項中,在兩個執行緒thread1和thread2分別得到一把鎖後,然後線上程1中請求執行緒2得到的那把鎖,執行緒2中請求線上程1中得到的那把鎖,由於兩個執行緒都在請求對方的鎖,但卻沒有一方釋放它們的鎖,所以就會出現死鎖的情況,程式執行後就會出現下面的結果:
6

程式與執行緒的區別

  • 地址空間和其他資源:程式間相互獨立,統一程式的各執行緒間共享。
  • 通訊:程式間通訊IPC,執行緒間可以直接讀寫程式資料段(如全域性變數)來進行通訊,但需要通過執行緒同步和互斥來保證資料的一致性。
  • 排程和切換:執行緒上下文切換比程式上下文切換要快的多。

相關文章