Python線程5分鐘徹底解讀

線程,有時被稱爲輕量進程,是程序執行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程不擁有私有的系統資源,但它可與同屬一個進程的其它線程共享進程所擁有的所有資源。一個線程能夠建立和撤消另外一個線程,同一進程中的多個線程之間能夠併發執行。python

線程是程序中一個單一的順序控制流程。進程內有一個相對獨立的、可調度的執行單元,是系統獨立調度和分派CPU的基本單位指令運行時的程序的調度單位。在單個程序中同時運行多個線程完成不一樣的工做,稱爲多線程。Python多線程用於I/O操做密集型的任務,如SocketServer網絡併發,網絡爬蟲。數據庫

現代處理器都是多核的,幾核處理器只能同時處理幾個線程,多線程執行程序看起來是同時進行,其實是CPU在多個線程之間快速切換執行,這中間就涉及到上下問切換,所謂的上下文切換就是指一個線程Thread被分配的時間片用完了以後,線程的信息被保存起來,CPU執行另外的線程,再到CPU讀取線程Thread的信息並繼續執行Thread的過程。編程

線程模塊安全

Python的標準庫提供了兩個模塊:_thread和threading。_thread 提供了低級別的、原始的線程以及一個簡單的互斥鎖,它相比於 threading 模塊的功能仍是比較有限的。Threading模塊是_thread模塊的替代,在實際的開發中,絕大多數狀況下仍是使用高級模塊threading,所以本書着重介紹threading高級模塊的使用。網絡

Python建立Thread對象語法以下:多線程

import threading
threading.Thread(target=None, name=None,  args=())
複製代碼

主要參數說明:併發

  • target 是函數名字,須要調用的函數。
  • name 設置線程名字。
  • args 函數須要的參數,以元祖( tuple)的形式傳入
  • Thread對象主要方法說明:
  • run(): 用以表示線程活動的方法。
  • start():啓動線程活動。
  • join(): 等待至線程停止。
  • isAlive(): 返回線程是否活動的。
  • getName(): 返回線程名。
  • setName(): 設置線程名。

Python中實現多線程有兩種方式:函數式建立線程和建立線程類。dom

第一種建立線程方式:函數

建立線程的時候,只須要傳入一個執行函數和函數的參數便可完成threading.Thread實例的建立。下面的例子使用Thread類來產生2個子線程,而後啓動2個子線程並等待其結束,學習

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading
import time,random,math

# idx 循環次數
def printNum(idx):
    for num in range(idx ):
#打印當前運行的線程名字
        print("{0}\tnum={1}".format(threading.current_thread().getName(), num) )
        delay = math.ceil(random.random() * 2)
        time.sleep(delay)

if __name__ == '__main__':
    th1 = threading.Thread(target=printNum, args=(2,),name="thread1"  )
    th2 = threading.Thread(target=printNum, args=(3,),name="thread2" )
#啓動2個線程
th1.start()
    th2.start()
#等待至線程停止
    th1.join()
    th2.join()
    print("{0} 線程結束".format(threading.current_thread().getName()))
複製代碼

運行腳本獲得如下結果。

thread1 num=0
thread2 num=0
thread1 num=1
thread2 num=1
thread2 num=2
複製代碼

MainThread 線程結束

運行腳本默認會啓動一個線程,把該線程稱爲主線程,主線程有能夠啓動新的線程,Python的threading模塊有個current_thread()函數,它將返回當前線程的示例。從當前線程的示例能夠得到前運行線程名字,核心代碼以下。

threading.current_thread().getName()
複製代碼

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

th1 = threading.Thread(target=printNum, args=(2,),name="thread1"  )
th1.start()
複製代碼

從返回結果能夠看出主線程示例的名字叫MainThread,子線程的名字在建立時指定,本例建立了2個子線程,名字叫thread1和thread2。若是沒有給線程起名字,Python就自動給線程命名爲Thread-1,Thread-2…等等。在本例中定義了線程函數printNum(),打印idx次記錄後退出,每次打印使用time.sleep()讓程序休眠一段時間。

第二種建立線程方式:建立線程類

直接建立threading.Thread的子類來建立一個線程對象,實現多線程。經過繼承Thread類,並重寫Thread類的run()方法,在run()方法中定義具體要執行的任務。在Thread類中,提供了一個start()方法用於啓動新進程,線程啓動後會自動調用run()方法。

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading
import time,random,math

class MutliThread(threading.Thread):

    def __init__(self, threadName,num):
        threading.Thread.__init__(self)
        self.name = threadName
        self.num = num

    def run(self):
        for i in range(self.num):
            print("{0} i={1}".format(threading.current_thread().getName(), i))
            delay = math.ceil(random.random() * 2)
            time.sleep(delay)

if __name__ == '__main__':
    thr1 = MutliThread("thread1",3)
    thr2 = MutliThread("thread2",2)
    # 啓動線程
    thr1.start()
    thr2.start()
    # 等待至線程停止
    thr1.join()
    thr2.join()
    print("{0} 線程結束".format(threading.current_thread().getName()))
運行腳本獲得如下結果。

thread1 i=0
thread2 i=0
thread1 i=1
thread2 i=1
thread1 i=2
複製代碼

MainThread 線程結束 從返回結果能夠看出,經過建立Thread類來產生2個線程對象thr1和thr2,重寫Thread類的run()函數,把業務邏輯放入其中,經過調用線程對象的start()方法啓動線程。經過調用線程對象的join()函數,等待該線程完成,在繼續下面的操做。

在本例中,主線程MainThread等待子線程thread1和thread2線程運行結束後才輸出」 MainThread 線程結束」。若是子線程thread1和thread2不調用join()函數,那麼主線程MainThread和2個子線程是並行執行任務的,2個子線程加上join()函數後,程序就變成順序執行了。因此子線程用到join()的時候,一般都是主線程等到其餘多個子線程執行完畢後再繼續執行,其餘的多個子線程並不須要互相等待。

守護線程

在線程模塊中,使用子線程對象用到join()函數,主線程須要依賴子線程執行完畢後才繼續執行代碼。若是子線程不使用join()函數,主線程和子線程是並行運行的,沒有依賴關係,主線程執行了,子線程也在執行。

在多線程開發中,若是子線程設定爲了守護線程,守護線程會等待主線程運行完畢後被銷燬。一個主線程能夠設置多個守護線程,守護線程運行的前提是,主線程必須存在,若是主線程不存在了,守護線程會被銷燬。

在本例中建立1個主線程3個子線程,讓主線程和子線程並行執行。內容以下。

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading, time

def run(taskName):
    print("任務:", taskName)
    time.sleep(2)
    print("{0} 任務執行完畢".format(taskName))  # 查看每一個子線程

if __name__ == '__main__':
    start_time = time.time()
    for i in range(3):
        thr = threading.Thread(target=run, args=("task-{0}".format(i),))
        # 把子線程設置爲守護線程
        thr.setDaemon(True)
        thr.start()

    # 查看主線程和當前活動的全部線程數
    print("{0}線程結束,當線程數量={1}".format( threading.current_thread().getName(), threading.active_count()))
    print("消耗時間:", time.time() - start_time)

運行腳本獲得如下結果:

任務: task-0
任務: task-1
任務: task-2
MainThread線程結束,當線程數量=4
消耗時間: 0.0009751319885253906
task-2 任務執行完畢
task-0 任務執行完畢
task-1 任務執行完畢
複製代碼

從返回結果能夠看出,當前的線程個數是4,線程個數=主線程數 + 子線程數,在本例中有1個主線程和3個子線程。主線程執行完畢後,等待子線程執行完畢,程序纔會退出。

在本例的基礎上,把全部的子線程都設置爲守護線程。子線程變成守護線程後,只要主線程執行完畢,程序無論子線程有沒有執行完畢,程序都會退出。使用線程對象的setDaemon(True)函數來設置守護線程。

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading, time

def run(taskName):
    print("任務:", taskName)
    time.sleep(2)
    print("{0} 任務執行完畢".format(taskName))

if __name__ == '__main__':
    start_time = time.time()
    for i in range(3):
        thr = threading.Thread(target=run, args=("task-{0}".format(i),))
        # 把子線程設置爲守護線程,在啓動線程前設置
thr.setDaemon(True)
        thr.start()

    # 查看主線程和當前活動的全部線程數
    thrName = threading.current_thread().getName()
    thrCount = threading.active_count()
    print("{0}線程結束,當線程數量={1}".format(thrName, thrCount))
    print("消耗時間:", time.time() - start_time)
運行腳本獲得如下結果。

任務: task-0
任務: task-1
任務: task-2
MainThread線程結束,當線程數量=4
消耗時間: 0.0010023117065429688
複製代碼

從本例的返回結果能夠看出,主線程執行完畢後,程序不會等待守護線程執行完畢後就退出了。設置線程對象爲守護線程,必定要在線程對象調用start()函數前設置。

多線程的鎖機制

多線程編程訪問共享變量時會出現問題,可是多進程編程訪問共享變量不會出現問題。由於多進程中,同一個變量各自有一份拷貝存在於每一個進程中,互不影響,而多線程中,全部變量都由全部線程共享。 多個進程之間對內存中的變量不會產生衝突,一個進程由多個線程組成,多線程對內存中的變量進行共享時會產生影響,因此就產生了死鎖問題,怎麼解決死鎖問題是本節主要介紹的內容。

一、變量的做用域

通常在函數體外定義的變量稱爲全局變量,在函數內部定義的變量稱爲局部變量。全局變量全部做用域均可讀,局部變量只能在本函數可讀。函數在讀取變量時,優先讀取函數自己自有的局部變量,再去讀全局變量。 內容以下。

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
# 全局變量
balance = 1

def change():
    # 定義全局變量
    global balance
    balance = 100
    # 定義局部變量
    num = 20
    print("change() balance={0}".format(balance) )

if __name__ == "__main__" :
    change()
    print("修改後的 balance={0}".format(balance) )
運行腳本獲得如下結果。

change() balance=100
修改後的 balance=100
複製代碼

若是註釋掉change()函數裏的 global

v1,那麼獲得的返回值是。
change() balance=100
修改後的 balance=1
複製代碼

在本例中在change()函數外定義的變量balance是全局變量,在change()函數內定義的變量num是局部變量,全局變量默認是可讀的,能夠在任何函數中使用,若是須要改變全局變量的值,須要在函數內部使用global定義全局變量,本例中在change()函數內部使用global定義全局變量balance,在函數裏就能夠改變全局變量了。

在函數裏可使用全局變量,可是在函數裏不能改變全局變量。想實現多個線程共享變量,須要使用全局變量。在方法里加上全局關鍵字 global定義全局變量,多線程才能夠修改全局變量來共享變量。

二、多線程中的鎖

多線程同時修改全局變量時會出現數據安全問題,線程不安全就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據。在本例中咱們生成2個線程同時修改change()函數裏的全局變量balance時,會出現數據不一致問題。

本案例文件名爲PythonFullStack\Chapter03\threadDemo03.py,內容以下。

import threading

balance = 100

def change(num, counter):
    global balance
    for i in range(counter):
        balance += num
        balance -= num
        if balance != 100:
            # 若是輸出這句話,說明線程不安全
            print("balance=%d" % balance)
break

if __name__ == "__main__":
    thr1 = threading.Thread(target=change,args=(100,500000),name='t1')
    thr2 = threading.Thread(target=change,args=(100,500000),name='t2')
    thr1.start()
    thr2.start()
    thr1.join()
    thr2.join()
    print("{0} 線程結束".format(threading.current_thread().getName()))
運行以上腳本,當2個線程運行次數達到500000次時,會出現如下結果。

balance=200
MainThread 線程結束
複製代碼

在本例中定義了一個全局變量balance,初始值爲100,當啓動2個線程後,先加後減,理論上balance應該爲100。線程的調度是由操做系統決定的,當線程t1和t2交替執行時,只要循環次數足夠多,balance結果就不必定是100了。從結果能夠看出,在本例中線程t1和t2同時修改全局變量balance時,會出現數據不一致問題。

注意

在多線程狀況下,全部的全局變量有全部線程共享。因此,任何一個變量均可以被任何一個線程修改,所以,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。

在多線程狀況下,使用全局變量並不會共享數據,會出現線程安全問題。線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致

在單線程運行時沒有代碼安全問題。寫多線程程序時,生成一個線程並不表明多線程。在多線程狀況下,纔會出現安全問題。

針對線程安全問題,須要使用」互斥鎖」,就像數據庫裏操縱數據同樣,也須要使用鎖機制。某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲「鎖定」,其餘線程不能更改;直到該線程釋放資源,將資源的狀態變成「非鎖定」,其餘的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操做,從而保證了多線程狀況下數據的正確性。

互斥鎖的核心代碼以下:

# 建立鎖
mutex = threading.Lock()

# 鎖定
mutex.acquire()

# 釋放
mutex.release()
複製代碼

若是要確保balance計算正確,使用threading.Lock()來建立鎖對象lock,把 lock.acquire()和lock.release()加在同步代碼塊裏,本例的同步代碼塊就是對全局變量balance進行先加後減操做。

當某個線程執行change()函數時,經過lock.acquire()獲取鎖,那麼其餘線程就不能執行同步代碼塊了,只能等待知道鎖被釋放了,得到鎖才能執行同步代碼塊。因爲鎖只有一個,不管多少線程,同一個時刻最多隻有一個線程持有該鎖,因此修改全局變量balance不會產生衝突。改良後的代碼內容以下。

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading

balance = 100
lock = threading.Lock()

def change(num, counter):
    global balance
    for i in range(counter):
# 先要獲取鎖
        lock.acquire()
        balance += num
        balance -= num
# 釋放鎖
lock.release()

        if balance != 100:
            # 若是輸出這句話,說明線程不安全
            print("balance=%d" % balance)
            break

if __name__ == "__main__":
    thr1 = threading.Thread(target=change,args=(100,500000),name='t1')
    thr2 = threading.Thread(target=change,args=(100,500000),name='t2')
    thr1.start()
    thr2.start()
    thr1.join()
    thr2.join()
    print("{0} 線程結束".format(threading.current_thread().getName()))
複製代碼

在本例中2個線程同時運行lock.acquire()時,只有一個線程能成功的獲取鎖,而後執行代碼,其餘線程就繼續等待直到得到鎖位置。得到鎖的線程用完後必定要釋放鎖,不然其餘線程就會一直等待下去,成爲死線程。

在運行上面腳本就不會產生輸出信息,證實代碼是安全的。把 lock.acquire()和lock.release()加在同步代碼塊裏,還要注意鎖的力度不要加的太大了。第一個線程只有運行完了,第二個線程才能運行,因此鎖要在須要同步代碼里加上。

相關文章
相關標籤/搜索