鍍金池/ 教程/ Python/ 線程
基礎
itertools
HTTP 服務
hashlib
閉包
文件和目錄
單元測試
使用 @property
標準模塊
陌生的 metaclass
Base64
進程、線程和協(xié)程
讀寫二進制文件
匿名函數(shù)
輸入和輸出
Click
元組
字符編碼
partial 函數(shù)
參考資料
collections
協(xié)程
類和實例
Python 之旅
定制類和魔法方法
常用數(shù)據(jù)類型
繼承和多態(tài)
ThreadLocal
HTTP 協(xié)議簡介
Requests 庫的使用
讀寫文本文件
列表
os 模塊
迭代器 (Iterator)
正則表達式
集合
上下文管理器
異常處理
你不知道的 super
定義函數(shù)
datetime
資源推薦
字典
slots 魔法
hmac
第三方模塊
進程
類方法和靜態(tài)方法
函數(shù)參數(shù)
高階函數(shù)
函數(shù)
re 模塊
高級特性
線程
argparse
生成器
結(jié)束語
字符串
map/reduce/filter
函數(shù)式編程
Celery
裝飾器

線程

線程(thread)是進程(process)中的一個實體,一個進程至少包含一個線程。比如,對于視頻播放器,顯示視頻用一個線程,播放音頻用另一個線程。如果我們把進程看成一個容器,則線程是此容器的工作單位。

進程和線程的區(qū)別主要有:

  • 進程之間是相互獨立的,多進程中,同一個變量,各自有一份拷貝存在于每個進程中,但互不影響;而同一個進程的多個線程是內(nèi)存共享的,所有變量都由所有線程共享;
  • 由于進程間是獨立的,因此一個進程的崩潰不會影響到其他進程;而線程是包含在進程之內(nèi)的,線程的崩潰就會引發(fā)進程的崩潰,繼而導致同一進程內(nèi)的其他線程也奔潰;

多線程

在 Python 中,進行多線程編程的模塊有兩個:thread 和 threading。其中,thread 是低級模塊,threading 是高級模塊,對 thread 進行了封裝,一般來說,我們只需使用 threading 這個模塊。

下面,我們看一個簡單的例子:

from threading import Thread, current_thread

def thread_test(name):
    print 'thread %s is running...' % current_thread().name
    print 'hello', name
    print 'thread %s ended.' % current_thread().name

if __name__ == "__main__":
    print 'thread %s is running...' % current_thread().name
    print 'hello world!'
    t = Thread(target=thread_test, args=("test",), name="TestThread")
    t.start()
    t.join()
    print 'thread %s ended.' % current_thread().name

可以看到,創(chuàng)建一個新的線程,就是把一個函數(shù)和函數(shù)參數(shù)傳給 Thread 實例,然后調(diào)用 start 方法開始執(zhí)行。代碼中的 current_thread 用于返回當前線程的實例。

執(zhí)行結(jié)果如下:

thread MainThread is running...
hello world!
thread TestThread is running...
hello test
thread TestThread ended.
thread MainThread ended.

由于同一個進程之間的線程是內(nèi)存共享的,所以當多個線程對同一個變量進行修改的時候,就會得到意想不到的結(jié)果。

讓我們先看一個簡單的例子:

from threading import Thread, current_thread

num = 0

def calc():
    global num
    print 'thread %s is running...' % current_thread().name
    for _ in xrange(10000):
        num += 1
    print 'thread %s ended.' % current_thread().name

if __name__ == '__main__':
    print 'thread %s is running...' % current_thread().name

    threads = []
    for i in range(5):
        threads.append(Thread(target=calc))
        threads[i].start()
    for i in range(5):
        threads[i].join()

    print 'global num: %d' % num
    print 'thread %s ended.' % current_thread().name

在上面的代碼中,我們創(chuàng)建了 5 個線程,每個線程對全局變量 num 進行 10000 次的 加 1 操作,這里之所以要循環(huán) 10000 次,是為了延長單個線程的執(zhí)行時間,使線程執(zhí)行時能出現(xiàn)中斷切換的情況。現(xiàn)在問題來了,當這 5 個線程執(zhí)行完畢時,全局變量的值是多少呢?是 50000 嗎?

讓我們看下執(zhí)行結(jié)果:

thread MainThread is running...
thread Thread-34 is running...
thread Thread-34 ended.
thread Thread-35 is running...
thread Thread-36 is running...
thread Thread-37 is running...
thread Thread-38 is running...
thread Thread-35 ended.
thread Thread-38 ended.
thread Thread-36 ended.
thread Thread-37 ended.
global num: 30668
thread MainThread ended.

我們發(fā)現(xiàn) num 的值是 30668,事實上,num 的值是不確定的,你再運行一遍,會發(fā)現(xiàn)結(jié)果變了。

原因是因為 num += 1 不是一個原子操作,也就是說它在執(zhí)行時被分成若干步:

  • 計算 num + 1,存入臨時變量 tmp 中;
  • 將 tmp 的值賦給 num.

由于線程是交替運行的,線程在執(zhí)行時可能中斷,就會導致其他線程讀到一個臟值。

為了保證計算的準確性,我們就需要給 num += 1 這個操作加上。當某個線程開始執(zhí)行這個操作時,由于該線程獲得了鎖,因此其他線程不能同時執(zhí)行該操作,只能等待,直到鎖被釋放,這樣就可以避免修改的沖突。創(chuàng)建一個鎖可以通過 threading.Lock() 來實現(xiàn),代碼如下:

from threading import Thread, current_thread, Lock

num = 0
lock = Lock()

def calc():
    global num
    print 'thread %s is running...' % current_thread().name
    for _ in xrange(10000):
        lock.acquire()    # 獲取鎖
        num += 1
        lock.release()    # 釋放鎖
    print 'thread %s ended.' % current_thread().name

if __name__ == '__main__':
    print 'thread %s is running...' % current_thread().name

    threads = []
    for i in range(5):
        threads.append(Thread(target=calc))
        threads[i].start()
    for i in range(5):
        threads[i].join()

    print 'global num: %d' % num
    print 'thread %s ended.' % current_thread().name

讓我們看下執(zhí)行結(jié)果:

thread MainThread is running...
thread Thread-44 is running...
thread Thread-45 is running...
thread Thread-46 is running...
thread Thread-47 is running...
thread Thread-48 is running...
thread Thread-45 ended.
thread Thread-47 ended.
thread Thread-48 ended.
thread Thread-46 ended.
thread Thread-44 ended.
global num: 50000
thread MainThread ended.

GIL 鎖

講到 Python 中的多線程,就不得不面對 GIL 鎖,GIL 鎖的存在導致 Python 不能有效地使用多線程實現(xiàn)多核任務,因為在同一時間,只能有一個線程在運行。

GIL 全稱是 Global Interpreter Lock,譯為全局解釋鎖。早期的 Python 為了支持多線程,引入了 GIL 鎖,用于解決多線程之間數(shù)據(jù)共享和同步的問題。但這種實現(xiàn)方式后來被發(fā)現(xiàn)是非常低效的,當大家試圖去除 GIL 的時候,卻發(fā)現(xiàn)大量庫代碼已重度依賴 GIL,由于各種各樣的歷史原因,GIL 鎖就一直保留到現(xiàn)在。

小結(jié)

  • 一個程序至少有一個進程,一個進程至少有一個線程。
  • 進程是操作系統(tǒng)分配資源(比如內(nèi)存)的最基本單元,線程是操作系統(tǒng)能夠進行調(diào)度和分派的最基本單元。
  • 在 Python 中,進行多線程編程的模塊有兩個:thread 和 threading。其中,thread 是低級模塊,threading 是高級模塊,對 thread 進行了封裝,一般來說,我們只需使用 threading 這個模塊。
  • 在執(zhí)行多線程操作時,注意加鎖。

參考資料

下一篇:結(jié)束語