Python多線程

首先引用廖老師的一句話:Python解釋器由于設計時有GIL全局鎖,導致了多線程無法利用多核。多線程的并行在Python中就是一個美麗的夢。

線程是進程的一部分, 每個線程也有它自身的產生、存在和消亡的過程,多線程都可以執行多個任務。任何進程默認就會啟動一個線程,我們把該線程稱為主線程(MainThread),主線程又可以啟動新的線程。

Python的線程雖然是真正的線程,但解釋器執行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執行前,必須先獲得GIL鎖,然后,每執行100條字節碼,解釋器就自動釋放GIL鎖,執行別的線程。這個GIL全局鎖實際上把所有線程的執行代碼都給上了鎖,所以,多線程在Python中只能交替執行,即使100個線程跑在100核CPU上,也只能用到1個核。

線程的優點:
1, 比單線程運行速度快。
2, 共享內存和變量,資源消耗少。

線程的缺點:
1, 線程之間容易發生死鎖,產生數據錯亂。

線程的狀態圖:


image.png

創建線程

Python中常使用的線程模塊

  • thread(低版本使用的),threading是高級模塊,對thread進行了封裝。絕大多數情況下,只需要使用threading這個高級模塊。
  • Queue
  • multiprocessing

Thread是threading模塊中最重要的類之一,可以使用它來創建線程。創建新的線程有兩種方法:
1, 直接創建threading.Thread類的對象,初始化時將可調用對象作為參數傳入。
2, 通過繼承Thread類,重寫它的run方法。

  • 直接創建threading.Thread類的對象創建新線程 / 守護進程
#encoding: utf-8
'''
    使用threading.Thread類創建新線程。
    構造方法:
        __init__(group=None, target=None, name=None, args=(), kwargs=None, verbose=None)
    參數說明: 
        group:線程組,目前還沒有實現,庫引用中提示必須是None。 
        target:要執行的方法; 
        name:線程名; 
        args/kwargs:要傳入方法的參數。
'''
import threading
import time
def func():
    print '當前運行的是子進程 {}'.format(threading.current_thread().name)
    time.sleep(1)
    print '子進程 {} 運行結束'.format(threading.current_thread().name)

if __name__ == '__main__':
    print '當前運行的是主進程 {}'.format(threading.current_thread().name)
    t = threading.Thread(target=func, name='t-1')

    t1 = time.time()
    #t.setDaemon(True)       # 將當前線程設置為守護線程,程序會等待【非守護線程】結束才退出,不會等【守護線程】。
    t.start()                #啟動線程
    print t.is_alive()
    t.join(2)                # 設置等待時間為2s,超過指定時間就會殺死子進程,默認為空,即一直等待子進程結束才往下接續運行。
    print t.is_alive()
    print '用時:',time.time()-t1
    print '主進程 {} 運行結束'.format(threading.current_thread().name)

輸出:

當前運行的是主進程 MainThread
當前運行的是子進程 t-1
子進程 t-1 運行結束
用時: 1.07799983025
主進程 MainThread 運行結束

注意:
t.getName()#獲得線程對象名稱。
t.isAlive()#判斷線程是否還活著。
t.setDaemon()設置是否為守護線程。初始值從創建該線程的線程繼承而來,當沒有非守護線程仍在運行時,程序將終止。也可以通過t = Thread(target = func, args(1,), daemon = True)

  • 繼承自Thread類創建新線程。
#encoding: utf-8
'''
    繼承自Thread類創建新線程。
'''
from threading import Thread
import time

class MyThread(Thread):
    def __init__(self, a):
        super(MyThread, self).__init__()  # 調用Thread類的構造函數
        self.a = a
    def run(self):
        print 'sleep:',self.a
        # time.sleep(self.a)

if __name__ == '__main__':
    t1 = MyThread(2)
    t2 = MyThread(4)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

輸出:

sleep: 2
sleep: 4

注意:
繼承Thread類的新類MyThread構造函數中必須要調用父類的構造方法,這樣才能產生父類的構造函數中的參數,才能產生線程所需要的參數。新的類中如果需要別的參數,直接在其構造方法中加即可。
同時,新類中,在重寫父類的run方法時,它默認是不帶參數的,如果需要給它提供參數,需要在類的構造函數中指定,因為在線程執行的過程中,run方法時線程自己去調用的,不用我們手動調用,所以沒法直接給傳遞參數,只能在構造方法中設定好參數,然后再run方法中調用。

創建線程池并發執行

Python中線程與進程使用的同一模塊 multiprocessing。使用方法也基本相同,唯一不同的是,from multiprocessing import Pool這樣導入的Pool表示的是進程池;
from multiprocessing.dummy import Pool這樣導入的Pool表示的是線程池。這樣就可以實現線程里面的并發了。

#encoding: utf-8
'''
    創建線程池并發執行
'''
import time
from multiprocessing.dummy import Pool as ThreadPool

def func(ans):
    time.sleep(1)
    print ans

if __name__ == '__main__':
    l = [1,2,3,4,5]
    pool = ThreadPool(5) #創建5個容量的線程池并發執行

    t1 = time.time()
    pool.map(func, l)
    pool.close()
    pool.join()
    print '用時:',time.time() - t1

輸出:

45
 
321


用時: 1.09400010109

注意:這里的pool.map()函數,跟進程池的map函數用法一樣,也跟內建的map函數一樣。

把程序改為pool = ThreadPool(1)
輸出:


現在就相當于時單線程,一個方法執行完了再執行另一個方法。

再把程序改成pool = ThreadPool(10000)
輸出:



發現運行的時間沒有縮短反而變長了。
無論是多進程還是多線程,只要數量一多,效率肯定上不去,為什么呢?

我們打個比方,假設你不幸正在準備中考,每天晚上需要做語文、數學、英語、物理、化學這5科的作業,每項作業耗時1小時。

如果你先花1小時做語文作業,做完了,再花1小時做數學作業,這樣,依次全部做完,一共花5小時,這種方式稱為單任務模型,或者批處理任務模型。

假設你打算切換到多任務模型,可以先做1分鐘語文,再切換到數學作業,做1分鐘,再切換到英語,以此類推,只要切換速度足夠快,這種方式就和單核CPU執行多任務是一樣的了,以幼兒園小朋友的眼光來看,你就正在同時寫5科作業。

但是,切換作業是有代價的,比如從語文切到數學,要先收拾桌子上的語文書本、鋼筆(這叫保存現場),然后,打開數學課本、找出圓規直尺(這叫準備新環境),才能開始做數學作業。操作系統在切換進程或者線程時也是一樣的,它需要先保存當前執行的現場環境(CPU寄存器狀態、內存頁等),然后,把新任務的執行環境準備好(恢復上次的寄存器狀態,切換內存頁等),才能開始執行。這個切換過程雖然很快,但是也需要耗費時間。如果有幾千個任務同時進行,操作系統可能就主要忙著切換任務,根本沒有多少時間去執行任務了,這種情況最常見的就是硬盤狂響,點窗口無反應,系統處于假死狀態。

所以,多任務一旦多到一個限度,就會消耗掉系統所有的資源,結果效率急劇下降,所有任務都做不好。

就這個程序而言, l 列表長度為5,有5個線程就夠用了,但開了10000個線程做這件事,時間反而浪費在了線程之間的切換操作。

線程鎖

在并發情況下,指令執行的先后順序由內核決定。同一個線程內部,指令按照先后順序執行,但不同線程之間的指令很難說清除哪一個會先執行。因此要考慮多線程同步的問題。同步(synchronization)是指在一定的時間內只允許某一個線程訪問某個資源。
多線程和多進程最大的不同在于,多進程中,同一個變量,各自有一份拷貝存在于每個進程中,互不影響,而多線程中,所有變量都由所有線程共享,所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在于多個線程同時改一個變量,把內容給改亂了。

  • 看這個程序:
#encoding: utf-8
'''
    數據錯亂
'''
import time, threading

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

def change_it(n):
    global balance
    balance = balance + n
    balance = balance - n

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

if __name__ == '__main__':
    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 ,但是結果是 -2540


image.png

我們當然不希望存款變成了負的。這是因為在兩個線程執行過程中,存在同時訪問 change_it 函數的時候,而 balance = balance - n 語句在CPU中是分開拆分開執行的 :
先 balance-n 存入臨時變量
然后 balance = 臨時變量
這樣當兩條線程同時執行change_it 函數時就會發生一加一減的賦值或算數錯誤。所以賬戶余額就有可能負的。

  • 死鎖:假設有兩個全局資源,a和b,有兩個線程thread1,thread2. thread1占用a,想訪問b,但此時thread2占用b,想訪問a,兩個線程都不釋放此時擁有的資源,那么就會造成死鎖。

這兩種情況都可以用線程鎖輕松解決(一口氣寫這么多,好累啊~~)。

#encoding: utf-8
'''
    添加線程鎖保證某時刻 只有一個線程在執行某函數
'''
import time, threading

balance = 0

def change_it(n):
    global balance
    balance = balance + n
    balance = balance - n

def run_thread(n):
    for i in range(100000):
        try:
            lock.acquire()  # 添加鎖
            change_it(n)
        finally:
            lock.release()  # 釋放鎖
        
        # # 等同于
        # with lock:
        #     change_it(n)

if __name__ == '__main__':
    lock = threading.Lock()

    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)

輸出:



注意:
當多個線程同時執行lock.acquire()時,只有一個線程能成功地獲取鎖,然后繼續執行代碼,其他線程就繼續等待直到獲得鎖為止。

獲得鎖的線程用完后一定要釋放鎖,否則那些苦苦等待鎖的線程將永遠等待下去,成為死線程。所以我們用try...finally來確保鎖一定會被釋放。

鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行,壞處當然也很多,首先是阻止了多線程并發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地下降了。其次,由于可以存在多個鎖,不同的線程持有不同的鎖,并試圖獲取對方持有的鎖時,可能會造成死鎖,導致多個線程全部掛起,既不能執行,也無法結束,只能靠操作系統強制終止。

GIL(全局解釋鎖) and Lock(線程鎖)

image.png

Semaphore(信號量)

互斥鎖 同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據 ,比如廁所有3個坑,那最多只允許3個人上廁所,后面的人只能等里面有人出來了才能再進去。

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

import threading
import time

def func(n):
    semaphore.acquire()
    time.sleep(1)
    print("this thread is %s\n" % n)
    semaphore.release()

semaphore = threading.BoundedSemaphore(5)   # 信號量
for i in range(23):
    t = threading.Thread(target=func,args=(i,))
    t.start()

while threading.active_count() != 1:
    pass
    # print(threading.active_count()) #當前存活線程個數

else:print("all threads is done...")

ThreadLocal( 類似C語言中的結構體 )

#encoding: utf-8
import threading

local_school = threading.local()

def process_student():
    # 獲取當前線程關聯的student:
    std = local_school.student
    print 'Hello,{} in {}'.format(std,threading.current_thread().name)

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

if __name__ == '__main__':
    t1 = threading.Thread(target=process_name, args=('Alice',),name='Thread-A')
    t2 = threading.Thread(target=process_name, 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請求,用戶身份信息等,這樣一個線程的所有調用到的處理函數都可以非常方便地訪問這些資源。

一個ThreadLocal變量雖然是全局變量,但每個線程都只能讀寫自己線程的獨立副本,互不干擾。ThreadLocal解決了參數在一個線程中各個函數之間互相傳遞的問題。

生產者消費者模型

在并發編程中使用生產者和消費者模式能夠解決絕大多數并發問題。該模式通過平衡生產線程和消費線程的工作能力來提高程序的整體處理數據的速度。

為什么要使用生產者和消費者模式?

在線程世界里,生產者就是生產數據的線程,消費者就是消費數據的線程。在多線程開發當中,如果生產者處理速度很快,而消費者處理速度很慢,那么生產者就必須等待消費者處理完,才能繼續生產數據。同樣的道理,如果消費者的處理能力大于生產者,那么消費者就必須等待生產者。為了解決這個問題于是引入了生產者和消費者模式。

什么是生產者消費者模式?

生產者消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,所以生產者生產完數據之后不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列里取,阻塞隊列就相當于一個緩沖區,平衡了生產者和消費者的處理能力。

下面來學習一個最基本的生產者消費者模型的例子

import threading,time

import queue

q = queue.Queue(maxsize=10)

def Producer(name):
    count = 1
    while True:
        q.put("骨頭%s" % count)
        print("生產了骨頭",count)
        count +=1
        time.sleep(0.1)

def  Consumer(name):
    #while q.qsize()>0:
    while True:
        print("[%s] 取到[%s] 并且吃了它..." %(name, q.get()))
        time.sleep(1)

p = threading.Thread(target=Producer,args=("Alex",))
c = threading.Thread(target=Consumer,args=("ChengRonghua",))
c1 = threading.Thread(target=Consumer,args=("王森",))

p.start()
c.start()
c1.start()
生產了骨頭 1
[ChengRonghua] 取到[骨頭1] 并且吃了它...
生產了骨頭 2
[王森] 取到[骨頭2] 并且吃了它...
生產了骨頭 3
生產了骨頭 4
生產了骨頭 5
生產了骨頭 6
生產了骨頭 7
生產了骨頭 8
生產了骨頭 9
生產了骨頭 10
[ChengRonghua] 取到[骨頭3] 并且吃了它...
生產了骨頭 11
[王森] 取到[骨頭4] 并且吃了它...
生產了骨頭 12
生產了骨頭 13
生產了骨頭 14
[ChengRonghua] 取到[骨頭5] 并且吃了它...
生產了骨頭 15
生產了骨頭 16
...
...
...

總結:

Python多線程很適合用在IO密集型任務中。I/O密集型執行期間大部分是時間都用在I/O上,如web應用,數據庫I/O,較少時間用在CPU計算上。因此該應用場景可以使用Python多線程,當一個任務阻塞在IO操作上時,我們可以立即切換執行其他線程上執行其他IO操作請求。Python多線程在IO密集型任務中還是很有用處的,而對于計算密集型任務,應該使用Python多進程。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。