Python的線程

本文是基於Py2.X數據庫

線程

多任務能夠由多進程完成,也能夠由一個進程內的多線程完成。網絡

咱們前面提到了進程是由若干線程組成的,一個進程至少有一個線程。多線程

多線程相似於同時執行多個不一樣程序,多線程運行有以下優勢:併發

  1. 能夠把運行時間長的任務放到後臺去處理。
  2. 用戶界面能夠更加吸引人,好比用戶點擊了一個按鈕去觸發某些事件的處理,能夠彈出一個進度條來顯示處理的進度。
  3. 程序的運行速度可能加快。
  4. 在一些須要等待的任務實現上,如用戶輸人、文件讀寫和網絡收發數據等,線程就比較有用了。在這種狀況下咱們能夠釋放一些珍貴的資源,如內存佔用等。

Python的標準庫提供了兩個模塊: thread 和threading,thread 是低級模塊,threading是高級模塊,對thread 進行了封裝。絕大多數狀況下,咱們只須要使用threading這個高級模塊。socket

啓動一個線程就是把一個函數傳入並建立Thread實例,而後調用start()開始執行:

# -*- coding:utf-8 -*-
import time, threading

# 新線程執行的代碼:
def loop():
    print 'thread %s is running...' % threading.current_thread().name
    n = 0
    while n < 5:
        n = n + 1
        print 'thread %s >>> %s' % (threading.current_thread().name, n)
        time.sleep(1)
    print 'thread %s ended.' % threading.current_thread().name

print 'thread %s is running...' % threading.current_thread().name
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print 'thread %s ended.' % threading.current_thread().name

獲得:
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

因爲任何進程默認就會啓動一個線程,咱們把該線程稱爲主線程,主線程又能夠啓動新的線程,Python的threading模塊有個current_thread()函數,它永遠返回當前線程的實例。主線程實例的名字叫MainThread,子線程的名字在建立時指定,咱們用LoopThread命名子線程。名字僅僅在打印時用來顯示,徹底沒有其餘意義,若是不起名字Python就自動給線程命名爲Thread-1,Thread-2……ide

Lock

多線程和多進程最大的不一樣在於,多進程中,同一個變量,各自有一份拷貝存在於每一個進程中,互不影響,而多線程中,全部變量都由全部線程共享,因此,任何一個變量均可以被任何一個線程修改,所以,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。函數

import time, threading

# 假定這是你的銀行存款:
balance = 0

def change_it(n):
    # 先存後取,結果應該爲0:
    global balance
    balance = balance + n
    balance = balance - n

def run_thread(n):
    for i in range(100000):
        change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print balance

獲得:
46,
且每次運行結果都會不同。

咱們定義了一個共享變量balance,初始值爲0,而且啓動兩個線程,先存後取,理論上結果應該爲0,可是,因爲線程的調度是由操做系統決定的,當t一、t2交替執行時,只要循環次數足夠多,balance的結果就不必定是0了。oop

因爲彼此間的交替運算,因此結果會發生變化,若是是在銀行操做,一存一取就可能致使餘額不對,因此必須確保一個線程在修改balance的時候,別的線程必定不能改。ui

若是咱們要確保balance計算正確,就要給change_it()上一把鎖,當某個線程開始執行change_it()時,咱們說,該線程由於得到了鎖,所以其餘線程不能同時執行change_it(),只能等待,直到鎖被釋放後,得到該鎖之後才能改。因爲鎖只有一個,不管多少線程,同一時刻最多隻有一個線程持有該鎖,因此,不會形成修改的衝突。建立一個鎖就是經過threading.Lock()來實現:操作系統

修改後的代碼:

# -*- coding:utf-8 -*-
import time, threading

# 假定這是你的銀行存款:
balance = 0

def change_it(n):
    # 先存後取,結果應該爲0:
    global balance
    balance = balance + n
    balance = balance - n

lock = threading.Lock()

def run_thread(n):
    for i in range(100000):
        # 先要獲取鎖:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了必定要釋放鎖:
            lock.release()

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print balance

結果,不管怎麼執行都是0,這正是咱們指望的結果。

當多個線程同時執行lock.acquire()時,只有一個線程能成功地獲取鎖,而後繼續執行代碼,其餘線程就繼續等待直到得到鎖爲止。

得到鎖的線程用完後必定要釋放鎖,不然那些苦苦等待鎖的線程將永遠等待下去,成爲死線程。因此咱們用try...finally來確保鎖必定會被釋放。

鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭至尾完整地執行,壞處固然也不少,首先是阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地降低了。其次,因爲能夠存在多個鎖,不一樣的線程持有不一樣的鎖,並試圖獲取對方持有的鎖時,可能會形成死鎖,致使多個線程所有掛起,既不能執行,也沒法結束,只能靠操做系統強制終止。

全局解釋器

若是你不幸擁有一個多核CPU,你確定在想,多核應該能夠同時執行多個線程。

在Python的原始解釋器CPython中存在着GIL(Global Interpreter Lock,全局解釋器鎖)所以在解釋執行Python代碼時,會產生互斥鎖來限制線程對共享資源的訪問,直到解釋器遇到I/O操做或者操做次數達到必定數據時支委會釋放GIL。因爲全局器鎖的存在,在進行多線程操做的時候,不能調用多個CPU內核,只能利用一個內核,因此在進行CPU密集型操做的時候,不推薦使用多線程,更加傾向於多進程,那麼多線程適合什麼樣的應用場景呢?對於IO密集型操做,多線程能夠明顯提升效率,例如Python爬蟲的開發,絕大多數時間爬蟲是在等待socket返回數據,網絡IO操做延時比CPU大得多。

ThreadLocal

在多線程環境下,每一個線程都有本身的數據。一個線程使用本身的局部變量比使用全局變量好,由於局部變量只有線程本身能看見,不會影響其餘線程,而全局變量的修改必須加鎖。

可是局部變量也有問題,就是在函數調用的時候,傳遞起來很麻煩:

def process_student(name):
    std = Student(name)
    # std是局部變量,可是每一個函數都要用它,所以必須傳進去:
    do_task_1(std)
    do_task_2(std)

def do_task_1(std):
    do_subtask_1(std)
    do_subtask_2(std)

def do_task_2(std):
    do_subtask_2(std)
    do_subtask_2(std)

每一個函數一層一層調用都這麼傳參數那還得了?用全局變量?也不行,由於每一個線程處理不一樣的Student對象,不能共享。

若是用一個全局dict存放全部的Student對象,而後以thread自身做爲key得到線程對應的Student對象如何?

global_dict = {}

def std_thread(name):
    std = Student(name)
    # 把std放到全局變量global_dict中:
    global_dict[threading.current_thread()] = std
    do_task_1()
    do_task_2()

def do_task_1():
    # 不傳入std,而是根據當前線程查找:
    std = global_dict[threading.current_thread()]
    ...

def do_task_2():
    # 任何函數均可以查找出當前線程的std變量:
    std = global_dict[threading.current_thread()]
    ...

這種方式理論上是可行的,它最大的優勢是消除了std對象在每層函數中的傳遞問題,可是,每一個函數獲取std的代碼有點醜。

有沒有更簡單的方式?

ThreadLocal應運而生,不用查找dict,ThreadLocal幫你自動作這件事:

import threading

# 建立全局ThreadLocal對象:
local_school = threading.local()

def process_student():
    print 'Hello, %s (in %s)' % (local_school.student, threading.current_thread().name)

def process_thread(name):
    # 綁定ThreadLocal的student:
    local_school.student = name
    process_student()

t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

獲得:

Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)

全局變量local_school就是一個ThreadLocal對象,每一個Thread對它均可以讀寫student屬性,但互不影響。你能夠把local_school當作全局變量,但每一個屬性如local_school.student都是線程的局部變量,能夠任意讀寫而互不干擾,也不用管理鎖的問題,ThreadLocal內部會處理。

能夠理解爲全局變量local_school是一個dict,不但能夠用local_school.student,還能夠綁定其餘變量,如local_school.teacher等等。

ThreadLocal最經常使用的地方就是爲每一個線程綁定一個數據庫鏈接,HTTP請求,用戶身份信息等,這樣一個線程的全部調用到的處理函數均可以很是方便地訪問這些資源。

相關文章
相關標籤/搜索