Python並行編程——多線程

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內容的文件對象。

 
class FetchUrls(threading.Thread):
  """
  下載URL內容的線程
  """

  def __init__(self, urls, output):
    """
    構造器

    @param urls 需要下載的URL列表
    @param output 寫URL內容的輸出文件
    """
    threading.Thread.__init__(self)
    self.urls = urls
    self.output = output

  def run(self):
    """
    實現父類Thread的run方法,打開URL,並且一個一個的下載URL的內容
    """
    while self.urls:
      url = self.urls.pop()
      req = urllib2.Request(url)
      try:
        d = urllib2.urlopen(req)
      except urllib2.URLError, e:
        print 'URL %s failed: %s' % (url, e.reason)
      self.output.write(d.read())
      print 'write done by %s' % self.name
      print 'URL %s fetched by %s' % (url, self.name)

main函數啓動了兩個線程,之後讓他們下載URL內容。

 
def main():
  # URL列表1
  urls1 = ['http://www.google.com', 'http://www.facebook.com']
  # URL列表2
  urls2 = ['http://www.yahoo.com', 'http://www.youtube.com']
  f = open('output.txt', 'w+')
  t1 = FetchUrls(urls1, f)
  t2 = FetchUrls(urls2, f)
  t1.start()
  t2.start()
  t1.join()
  t2.join()
  f.close()

if __name__ == '__main__':
  main()

上面的程序將出現兩個線程同時寫一個文件的情況,導致文件一團亂碼。我們需要找到一種在給定的時間裏只有一個線程寫文件的方法。實現的方法就是使用像鎖(Locks)這樣的線程同步機制。

鎖(Lock)

鎖有兩種狀態:被鎖(locked)和沒有被鎖(unlocked)。擁有acquire()和release()兩種方法,並且遵循一下的規則:

  • 如果一個鎖的狀態是unlocked,調用acquire()方法改變它的狀態爲locked;
  • 如果一個鎖的狀態是locked,acquire()方法將會阻塞,直到另一個線程調用release()方法釋放了鎖;
  • 如果一個鎖的狀態是unlocked調用release()會拋出RuntimeError異常;
  • 如果一個鎖的狀態是locked,調用release()方法改變它的狀態爲unlocked。

解決上面兩個線程同時寫一個文件的問題的方法就是:我們給類FetchUrls的構造器中傳入一個鎖(lock),使用這個鎖來保護文件操作,實現在給定的時間只有一個線程寫文件。下面的代碼只顯示了關於lock部分的修改。完整的源碼可以在threads/lock.py中找到。

 
class FetchUrls(threading.Thread):
  ...

  def __init__(self, urls, output, lock):
    ...
    self.lock = lock	#傳入的lock對象

  def run(self):
    ...
    while self.urls:
      ...
      self.lock.acquire()	#獲得lock對象,lock狀態變爲locked,並且阻塞其他線程獲取lock對象(寫文件的權利)
      print 'lock acquired by %s' % self.name
      self.output.write(d.read())
      print 'write done by %s' % self.name
      print 'lock released by %s' % self.name
      self.lock.release()	#釋放lock對象,lock狀態變爲unlocked,其他的線程可以重新獲取lock對象
      ...

def main():
  ...
  lock = threading.Lock()
  ...
  t1 = FetchUrls(urls1, f, lock)
  t2 = FetchUrls(urls2, f, lock)
  ...

以下是程序的輸出:

 
$ python locks.py
lock acquired by Thread-2
write done by Thread-2
lock released by Thread-2
URL http://www.youtube.com fetched by Thread-2
lock acquired by Thread-1
write done by Thread-1
lock released by Thread-1
URL http://www.facebook.com fetched by Thread-1
lock acquired by Thread-2
write done by Thread-2
lock released by Thread-2
URL http://www.yahoo.com fetched by Thread-2
lock acquired by Thread-1
write done by Thread-1
lock released by Thread-1
URL http://www.google.com fetched by Thread-1

從上面的輸出我們可以看出,寫文件的操作被鎖保護,沒有出現兩個線程同時寫一個文件的現象。

下面我們看一下Python內部是如何實現鎖(Lock)的。我正在使用的Python版本是Linux操作系統上的Python 2.6.6。

threading模塊的Lock()方法就是thread.allocate_lock,代碼可以在Lib/threading.py中找到。

 
Lock = _allocate_lock
_allocate_lock = thread.allocate_lock

C的實現在Python/thread_pthread.h中。程序假定你的系統支持POSIX信號量(semaphores)。sem_init()初始化鎖(Lock)所在地址的信號量。初始的信號量值是1,意味着鎖沒有被鎖(unlocked)。信號量將在處理器的不同線程之間共享。

 
PyThread_type_lock
PyThread_allocate_lock(void)
{
    ...
    lock = (sem_t *)malloc(sizeof(sem_t));

    if (lock) {
        status = sem_init(lock,0,1);
        CHECK_STATUS("sem_init");
        ....
    }
    ...
}

當acquire()方法被調用時,下面的C代碼將被執行。默認的waitflag值是1,表示調用將被被阻塞直到鎖被釋放。sem_wait()方法減少信號量的值或者被阻塞直到信號量大於零。

 
int
PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)
{
    ...
    do {
        if (waitflag)
            status = fix_status(sem_wait(thelock));
        else
            status = fix_status(sem_trywait(thelock));
    } while (status == EINTR); /* Retry if interrupted by a signal */
    ....
}

當release()方法被調用時,下面的C代碼將被執行。sem_post()方法增加信號量。

 
void
PyThread_release_lock(PyThread_type_lock lock)
{
    ...
    status = sem_post(thelock);
    ...
}

可以將鎖(Lock)與“with”語句一起使用,鎖可以作爲上下文管理器(context manager)。使用“with”語句的好處是:當程序執行到“with”語句時,acquire()方法將被調用,當程序執行完“with”語句時,release()方法會被調用(譯註:這樣我們就不用顯示地調用acquire()和release()方法,而是由“with”語句根據上下文來管理鎖的獲取和釋放。)下面我們用“with”語句重寫FetchUrls類。

 
class FetchUrls(threading.Thread):
  ...
  def run(self):
    ...
    while self.urls:
      ...
      with self.lock:	#使用“with”語句管理鎖的獲取和釋放
        print 'lock acquired by %s' % self.name
        self.output.write(d.read())
        print 'write done by %s' % self.name
        print 'lock released by %s' % self.name
      ...

可重入鎖(RLock)

RLock是可重入鎖(reentrant lock),acquire()能夠不被阻塞的被同一個線程調用多次。要注意的是release()需要調用與acquire()相同的次數才能釋放鎖。

使用Lock,下面的代碼第二次調用acquire()時將被阻塞:

 
lock = threading.Lock()
lock.acquire()
lock.acquire()

如果你使用的是RLock,下面的代碼第二次調用acquire()不會被阻塞:

 
rlock = threading.RLock()
rlock.acquire()
rlock.acquire()

RLock使用的同樣是thread.allocate_lock(),不同的是他跟蹤宿主線程(the owner thread)來實現可重入的特性。下面是RLock的acquire()實現。如果調用acquire()的線程是資源的所有者,記錄調用acquire()次數的計數器就會加1。如果不是,就將試圖去獲取鎖。線程第一次獲得鎖時,鎖的擁有者將會被保存,同時計數器初始化爲1。

 
def acquire(self, blocking=1):
    me = _get_ident()
    if self.__owner == me:
        self.__count = self.__count + 1
        ...
        return 1
    rc = self.__block.acquire(blocking)
    if rc:
        self.__owner = me
        self.__count = 1
        ...
    ...
    return rc

下面我們看一下可重入鎖(RLock)的release()方法。首先它會去確認調用者是否是鎖的擁有者。如果是的話,計數器減1;如果計數器爲0,那麼鎖將會被釋放,這時其他線程就可以去獲取鎖了。

 
def release(self):
    if self.__owner != _get_ident():
        raise RuntimeError("cannot release un-acquired lock")
    self.__count = count = self.__count - 1
    if not count:
        self.__owner = None
        self.__block.release()
        ...
    ...

條件(Condition)

條件同步機制是指:一個線程等待特定條件,而另一個線程發出特定條件滿足的信號。 解釋條件同步機制的一個很好的例子就是生產者/消費者(producer/consumer)模型。生產者隨機的往列表中“生產”一個隨機整數,而消費者從列表中“消費”整數。完整的源碼可以在threads/condition.py中找到

在producer類中,producer獲得鎖,生產一個隨機整數,通知消費者有了可用的“商品”,並且釋放鎖。producer無限地向列表中添加整數,同時在兩個添加操作中間隨機的停頓一會兒。

 
class Producer(threading.Thread):
  """
  向列表中生產隨機整數
  """

  def __init__(self, integers, condition):
    """
    構造器

    @param integers 整數列表
    @param condition 條件同步對象
    """
    threading.Thread.__init__(self)
    self.integers = integers
    self.condition = condition

  def run(self):
    """
    實現Thread的run方法。在隨機時間向列表中添加一個隨機整數
    """
    while True:
      integer = random.randint(0, 256)
      self.condition.acquire()	#獲取條件鎖
      print 'condition acquired by %s' % self.name
      self.integers.append(integer)
      print '%d appended to list by %s' % (integer, self.name)
      print 'condition notified by %s' % self.name
      self.condition.notify()	#喚醒消費者線程
      print 'condition released by %s' % self.name
      self.condition.release()	#釋放條件鎖
      time.sleep(1)		#暫停1秒鐘

下面是消費者(consumer)類。它獲取鎖,檢查列表中是否有整數,如果沒有,等待生產者的通知。當消費者獲取整數之後,釋放鎖。
注意在wait()方法中會釋放鎖,這樣生產者就能獲得資源並且生產“商品”。

 
class Consumer(threading.Thread):
  """
  從列表中消費整數
  """

  def __init__(self, integers, condition):
    """
    構造器

    @param integers 整數列表
    @param condition 條件同步對象
    """
    threading.Thread.__init__(self)
    self.integers = integers
    self.condition = condition

  def run(self):
    """
    實現Thread的run()方法,從列表中消費整數
    """
    while True:
      self.condition.acquire()	#獲取條件鎖
      print 'condition acquired by %s' % self.name
      while True:
        if self.integers:	#判斷是否有整數
          integer = self.integers.pop()
          print '%d popped from list by %s' % (integer, self.name)
          break
        print 'condition wait by %s' % self.name
        self.condition.wait()	#等待商品,並且釋放資源
      print 'condition released by %s' % self.name
      self.condition.release()	#最後釋放條件鎖

下面我們編寫main方法,創建兩個線程:

 
def main():
  integers = []
  condition = threading.Condition()
  t1 = Producer(integers, condition)
  t2 = Consumer(integers, condition)
  t1.start()
  t2.start()
  t1.join()
  t2.join()

if __name__ == '__main__':
  main()

下面是程序的輸出:

 
$ python condition.py
condition acquired by Thread-1
159 appended to list by Thread-1
condition notified by Thread-1
condition released by Thread-1
condition acquired by Thread-2
159 popped from list by Thread-2
condition released by Thread-2
condition acquired by Thread-2
condition wait by Thread-2
condition acquired by Thread-1
116 appended to list by Thread-1
condition notified by Thread-1
condition released by Thread-1
116 popped from list by Thread-2
condition released by Thread-2
condition acquired by Thread-2
condition wait by Thread-2

Thread-1添加159到列表中,通知消費者同時釋放鎖,Thread-2獲得鎖,取回159,並且釋放鎖。此時因爲執行time.sleep(1),生產者正在睡眠,當消費者再次試圖獲取整數時,列表中並沒有整數,這時消費者進入等待狀態,等待生產者的通知。當wait()被調用時,它會釋放資源,從而生產者能夠利用資源生產整數。

下面我們看一下Python內部是如何實現條件同步機制的。如果用戶沒有傳入鎖(lock)對象,condition類的構造器創建一個可重入鎖(RLock),這個鎖將會在調用acquire()和release()時使用。

 
class _Condition(_Verbose):

    def __init__(self, lock=None, verbose=None):
        _Verbose.__init__(self, verbose)
        if lock is None:
            lock = RLock()
        self.__lock = lock

接下來是wait()方法。爲了簡化說明,我們假定在調用wait()方法時不使用timeout參數。wait()方法創建了一個名爲waiter的鎖,並且設置鎖的狀態爲locked。這個waiter鎖用於線程間的通訊,這樣生產者(在生產完整數之後)就可以通知消費者釋放waiter()鎖。鎖對象將會被添加到等待者列表,並且在調用waiter.acquire()時被阻塞。一開始condition鎖的狀態被保存,並且在wait()結束時被恢復。

 
def wait(self, timeout=None):
    ...
    waiter = _allocate_lock()
    waiter.acquire()
    self.__waiters.append(waiter)
    saved_state = self._release_save()
    try:    # 無論如何恢復狀態 (例如, KeyboardInterrupt)
        if timeout is None:
            waiter.acquire()
            ...
        ...
    finally:
        self._acquire_restore(saved_state)

當生產者調用notify()方法時,notify()釋放waiter鎖,喚醒被阻塞的消費者。

 
def notify(self, n=1):
    ...
    __waiters = self.__waiters
    waiters = __waiters[:n]
    ...
    for waiter in waiters:
        waiter.release()
        try:
            __waiters.remove(waiter)
        except ValueError:
            pass

同樣Condition對象也可以和“with”語句一起使用,這樣“with”語句上下文會幫我們調用acquire()和release()方法。下面的代碼使用“with”語句改寫了生產者和消費者類。

 
class Producer(threading.Thread):
  ...
  def run(self):
    while True:
      integer = random.randint(0, 256)
      with self.condition:
        print 'condition acquired by %s' % self.name
        self.integers.append(integer)
        print '%d appended to list by %s' % (integer, self.name)
        print 'condition notified by %s' % self.name
        self.condition.notify()
        print 'condition released by %s' % self.name
      time.sleep(1)

class Consumer(threading.Thread):
  ...
  def run(self):
    while True:
      with self.condition:
        print 'condition acquired by %s' % self.name
        while True:
          if self.integers:
            integer = self.integers.pop()
            print '%d popped from list by %s' % (integer, self.name)
            break
          print 'condition wait by %s' % self.name
          self.condition.wait()
        print 'condition released by %s' % self.name

信號量(Semaphore)

信號量同步基於內部計數器,每調用一次acquire(),計數器減1;每調用一次release(),計數器加1.當計數器爲0時,acquire()調用被阻塞。這是迪科斯徹(Dijkstra)信號量概念P()和V()的Python實現。信號量同步機制適用於訪問像服務器這樣的有限資源。

信號量同步的例子:

 
semaphore = threading.Semaphore()
semaphore.acquire()
 # 使用共享資源
...
semaphore.release()

讓我們看一下信號量同步在Python內部是如何實現的。構造器使用參數value來表示計數器的初始值,默認值爲1。一個條件鎖實例用於保護計數器,同時當信號量被釋放時通知其他線程。

 
class _Semaphore(_Verbose):
    ...
    def __init__(self, value=1, verbose=None):
        _Verbose.__init__(self, verbose)
        self.__cond = Condition(Lock())
        self.__value = value
        ...

acquire()方法。如果信號量爲0,線程被條件鎖的wait()方法阻塞,直到被其他線程喚醒;如果計數器大於0,調用acquire()使計數器減1。

 
def acquire(self, blocking=1):
    rc = False
    self.__cond.acquire()
    while self.__value == 0:
        ...
        self.__cond.wait()
    else:
        self.__value = self.__value - 1
        rc = True
    self.__cond.release()
    return rc

信號量類的release()方法增加計數器的值並且喚醒其他線程。

 
def release(self):
    self.__cond.acquire()
    self.__value = self.__value + 1
    self.__cond.notify()
    self.__cond.release()

還有一個“有限”(bounded)信號量類,可以確保release()方法的調用次數不能超過給定的初始信號量數值(value參數),下面是“有限”信號量類的Python代碼:

 
class _BoundedSemaphore(_Semaphore):
    """檢查release()的調用次數是否小於等於acquire()次數"""
    def __init__(self, value=1, verbose=None):
        _Semaphore.__init__(self, value, verbose)
        self._initial_value = value

    def release(self):
        if self._Semaphore__value >= self._initial_value:
            raise ValueError, "Semaphore released too many times"
        return _Semaphore.release(self)

同樣信號量(Semaphore)對象可以和“with”一起使用:

 
semaphore = threading.Semaphore()
with semaphore:
  # 使用共享資源
  ...

事件(Event)

基於事件的同步是指:一個線程發送/傳遞事件,另外的線程等待事件的觸發。 讓我們再來看看前面的生產者和消費者的例子,現在我們把它轉換成使用事件同步而不是條件同步。完整的源碼可以在threads/event.py裏面找到。

首先是生產者類,我們傳入一個Event實例給構造器而不是Condition實例。一旦整數被添加進列表,事件(event)被設置和發送去喚醒消費者。注意事件(event)實例默認是被髮送的。

 
class Producer(threading.Thread):
  """
  向列表中生產隨機整數
  """

  def __init__(self, integers, event):
    """
    構造器

    @param integers 整數列表
    @param event 事件同步對象
    """
    threading.Thread.__init__(self)
    self.integers = integers
    self.event = event

  def run(self):
    """
    實現Thread的run方法。在隨機時間向列表中添加一個隨機整數
    """
    while True:
      integer = random.randint(0, 256)
      self.integers.append(integer)
      print '%d appended to list by %s' % (integer, self.name)
      print 'event set by %s' % self.name
      self.event.set()		#設置事件	
      self.event.clear()	#發送事件
      print 'event cleared by %s' % self.name
      time.sleep(1)

同樣我們傳入一個Event實例給消費者的構造器,消費者阻塞在wait()方法,等待事件被觸發,即有可供消費的整數。

 
class Consumer(threading.Thread):
  """
   從列表中消費整數
  """

  def __init__(self, integers, event):
    """
    構造器

    @param integers 整數列表
    @param event 事件同步對象
    """
    threading.Thread.__init__(self)
    self.integers = integers
    self.event = event

  def run(self):
    """
    實現Thread的run()方法,從列表中消費整數
    """
    while True:
      self.event.wait()	#等待事件被觸發
      try:
        integer = self.integers.pop()
        print '%d popped from list by %s' % (integer, self.name)
      except IndexError:
        # catch pop on empty list
        time.sleep(1)

下面是程序的輸出,Thread-1添加124到整數列表中,然後設置事件並且喚醒消費者。消費者從wait()方法中喚醒,在列表中獲取到整數。

 
$ python event.py
124 appended to list by Thread-1
event set by Thread-1
event cleared by Thread-1
124 popped from list by Thread-2
223 appended to list by Thread-1
event set by Thread-1
event cleared by Thread-1
223 popped from list by Thread-2

事件鎖的Python內部實現,首先是Event鎖的構造器。構造器中創建了一個條件(Condition)鎖,來保護事件標誌(event flag),同事喚醒其他線程當事件被設置時。

 
class _Event(_Verbose):
    def __init__(self, verbose=None):
        _Verbose.__init__(self, verbose)
        self.__cond = Condition(Lock())
        self.__flag = False

接下來是set()方法,它設置事件標誌爲True,並且喚醒其他線程。條件鎖對象保護程序修改事件標誌狀態的關鍵部分。

 
def set(self):
    self.__cond.acquire()
    try:
        self.__flag = True
        self.__cond.notify_all()
    finally:
        self.__cond.release()

而clear()方法正好相反,它設置時間標誌爲False。

 
def clear(self):
    self.__cond.acquire()
    try:
        self.__flag = False
    finally:
        self.__cond.release()

最後,wait()方法將阻塞直到調用了set()方法,當事件標誌爲True時,wait()方法就什麼也不做。

 
def wait(self, timeout=None):
    self.__cond.acquire()
    try:
        if not self.__flag:	#如果flag不爲真
            self.__cond.wait(timeout)
    finally:
        self.__cond.release()

 

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放入隊列中。

  1. 如果可選的參數block爲True且timeout爲空對象(默認的情況,阻塞調用,無超時)。
  2. 如果timeout是個正整數,阻塞調用進程最多timeout秒,如果一直無空空間可用,拋出Full異常(帶超時的阻塞調用)。
  3. 如果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密集型的問題還是能都明顯提高效率的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章