Python學習筆記(二十八):線程


進程和線程的區別:

  • 進程:是系統進行資源分配和調度的一個獨立單位;

  • 線程:是進程的一個實體,是 CPU 調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧);但是它可與同屬一個進程的其他線程共享進程所擁有的全部資源。

  • 一個程序至少有一個進程,一個進程至少有一個線程;

  • 線程的劃分尺度小於進程(資源比進程少),使得多線程程序的併發性高;

  • 進程在執行過程中,擁有獨立的內存單元;而多個線程共享一個內存單元,從而極大地提高了程序的運行效率;

  • 線程不能夠獨立運行,必須依存在進程中;

  • 線程和進程在使用上各有優缺點:線程執行開銷小,但不利於資源的管理和保護;而進程正相反;

  • 進程和線程都能夠完成多任務:比如一個電腦上可以安裝多個 QQ,多個 QQ 就是多個進程;而一個 QQ 可以打開多個聊天窗口,可以同時與多人聊天,這就是多線程。

  • 每個線程都有它自己的一組 CPU 寄存器,稱爲線程的上下文,該上下文反映了上次運行該線程的 CPU 寄存器狀態;

  • 線程可以被搶斷(中斷);

 

threading 模塊:

python 中用於線程的兩個模塊爲 _thread 和 threading;

thread 模塊已經被棄用,在 python3 中不能再使用 thread 模塊,爲了兼容性,python3 將 thread 模塊重命名爲 _thread;但是 _thread 模塊只提供了低級別的、原始的線程、以及一個簡單的鎖;而 threading 模塊除了包含 _thread 模塊中的所有方法外,還提供了其他的方法:

  • threading.currentThread():返回當前的線程對象;

  • threading.enumerate():返回一個包含正在運行的線程的 list;正在運行指線程啓動後,結束前;

  • threading.activeCount():返回正在運行的線程數量,與 len(threading.enumerate) 有相同的結果;

threading 模塊除了提供上面的方法外,還提供了 Thread 類來表示一個線程,Thread 類提供了以下方法:

  • run():用於執行線程功能的方法;

  • start():用於啓動線程;

  • join([time]):等待線程結束;

  • isAlive():返回線程是否活動的;

  • getName():返回線程名。默認線程名爲 Thread-N,N 是從 1 遞增的整數;

  • setName():設置線程名;

 

創建子線程方法一:

實例化 threading.Thread 對象,通過 target 參數指定線程執行的對象;

# 導入 threading 模塊
import threading
import time

# 聲明一個函數,在子線程中執行
def test(msg):
    for i in range(5):
        print("=== %s-%d ===" %(msg, i))
        print("thread_name:", threading.currentThread().getName())
        time.sleep(1)

if __name__ == "__main__":
    # 創建子線程,並指定子線程執行的對象(test函數)
    # args 參數用於向線程中傳入參數,以元祖的形式;
    t = threading.Thread(target=test, args=("hello", ))
    # 啓動子線程
    t.start()
    # join() 方法用於阻塞線程,即等待線程執行結束纔會繼續向下執行
    t.join()
    # print(t.getName())  # 獲取線程的名字

    print("=== main ===")

 

創建子線程方法二:

自定義一個類,繼承於 Thread 類,然後重寫 Thread 類的 run() 方法;

# 導入 threading 模塊
import threading
import time

# 自定義一個類,繼承於 Thread 類
class MyThread(threading.Thread):
    # 重寫 Thread 類的 run 方法,self 是當前線程對象
    def run(self):
        for i in range(5):
            print("=== %s ===" %(self.name))
            time.sleep(1)

if __name__ == "__main__":
    # 實例化自定義類的對象
    t = MyThread()
    # 啓動子線程,自動執行 run 方法
    t.start()

    print("=== main ===")

 

多線程共享全局變量:

多線程共享全局變量,可以通過在線程方法中聲明全局變量來實現:

# 導入 threading 模塊中的 Thread 類
from threading import Thread
import time

# 聲明一個全局變量
g_num = 100

# 聲明兩個函數 work1 和 work2,在子線程中執行
def work1():
    # 子線程中操作外部全局變量,需要用 global 關鍵字聲明全局變量
    global g_num
    g_num += 100
    print("---in work1, g_num is %d---" %g_num)

def work2():
    # 在 work1 線程中修改了全局變量 g_num 的值,
    # 也影響了 work2 中全局變量 g_num 的值;
    global g_num
    print("---in work2, g_num is %d---" %g_num)

if __name__ == "__main__":
    print("---線程創建之前 g_num is %d---" %g_num)

    # 創建子線程對象,指定線程執行的目標對象爲 work1 函數
    t1 = Thread(target=work1)
    t1.start()  # 啓動子線程

    # 延遲 1 秒,保證上一個線程先執行
    time.sleep(1)

    t2 = Thread(target=work2)
    t2.start()

多線程共享全局變量,還可以通過將全局變量傳入到線程中實現:注意,此方法只對可變類型的數據有效;

# 導入 threading 模塊中的 Thread 類
from threading import Thread
import time

# 聲明一個整數類型的全局變量(不可變類型)
g_num = 100
# 聲明一個列表類型的全局變量(可變類型)
g_nums = [11, 22, 33]

# 聲明兩個函數 work1 和 work2,在子線程中執行
def work1(g_num, g_nums):
    # 在子線程中修改傳入進來的變量
    g_num += 100
    g_nums.append(44);
    print("in work1, g_num is:", g_num)
    print("in work1, g_nums is:", g_nums)

def work2(g_num, g_nums):
    # 因爲 g_num 是整形的,是不可變類型,那麼在 work1 線程中
    # 修改 g_num 變量的值,並不會改變外部全局變量 g_num 的值;
    print("in work2, g_num is:", g_num)

    # 而 g_nums 是列表類型,是可變類型,那麼 在 work1 線程中
    # 修改 g_nums 的值,會導致外部全局變量 g_nums 的值也跟着
    # 改變,那麼傳入 work2 線程中的 g_nums 的值就是改變後的新值;
    print("in work2, g_nums is:", g_nums)

if __name__ == "__main__":
    print("線程創建之前 g_num is:", g_num)
    print("線程創建之前 g_nums is:", g_nums)

    # 創建子線程對象,指定線程執行的目標對象爲 work1 函數
    # args 參數表示傳入到線程中的數據,以元祖的形式表示;
    t1 = Thread(target=work1, args=(g_num, g_nums))
    t1.start()  # 啓動子線程

    # 延遲 1 秒,保證上一個線程先執行
    time.sleep(1)

    t2 = Thread(target=work2, args=(g_num, g_nums))
    t2.start()

 

多線程共享全局變量可能會遇到的問題:

先看下面一段代碼:

# 導入 threading 模塊的 Thread 類
from threading import Thread
import time

# 聲明一個全局變量
g_num = 0;

# 定義兩個方法,分別在兩個方法中對全局變量 g_num 自加 100萬次
def work1():
    global g_num
    for i in range(1000000):
        g_num += 1
    print("work1 ---- g_num:", g_num)

def work2():
    global g_num
    for i in range(1000000):
        g_num += 1
    print("work2 ---- g_num:", g_num)

if __name__ == "__main__":
    # 創建子線程,並指定子線程執行的對象
    t1 = Thread(target=work1)
    t1.start()

    # time.sleep(3) # 延遲 3 秒

    t2 = Thread(target=work2)
    t2.start()

輸出結果:

如果把代碼中 time.sleep(3) 延遲註釋放開,輸出結果變成了:

那麼爲什麼會有兩種不同的結果呢???

先看第二種結果:線程1 先執行,然後延遲了 3 秒,在這3秒時間裏,足夠線程1執行結束,所以線程1中輸出 g_num 的值爲 1000000,此時全局變量 g_num 的值也是 1000000;在3秒延遲結束之後,開始執行線程2,那麼此時線程2中的 g_num 變量的初始值就是 1000000,然後再讓其自加 100萬次,所以最終線程2中 g_num 變量的值爲 2000000;

那麼爲什麼把延遲3秒註釋掉,結果就不對了呢?這是因爲,沒有延遲的時候,兩個線程同時執行,那麼兩個線程就會同時操作全局變量 g_num;而我們知道,線程的執行順序是不確定的,那麼就有可能會出現這麼一個情況:假如 g_num = 100 的時候,線程1搶到了執行權,於是線程1獲取到 g_num 變量,此時線程1中 g_num 變量的值爲 100,但是線程1還沒來得及給 g_num 加1,線程2又搶到了執行權,那麼此時線程2獲取到的 g_num 的值也是 100,然後線程2給 g_num 加1之後,全局變量 g_num 的值變成了 101;然後線程1又搶到了執行權,雖然全局變量的值已經變成了 101,但是線程1之前已經獲取到了 g_num,而且 g_num 的值爲 100,於是線程1給 g_num 加1後,又再次把全局變量的值變成了 101;所以,雖然線程1和線程2都對 g_num 完成了一次加1操作,但是實際上全局變量 g_num 的值只是增加了 1,並沒有增加 2.

問題產生的原因就是沒有控制多個線程對同一資源的訪問,對數據造成了破壞,使得線程運行的結果不可預期,這種情況叫做 “線程不安全”。

解決這種線程安全問題的辦法是:線程同步;同步的意思是協同步調,即按照約定的先後順序依次執行;而不是同時執行的意思。

而線程同步的具體實施方案是:對共享資源添加互斥鎖;互斥鎖的原理如下:

1、當線程1獲取到執行權的時候,給全局變量 g_num 添加一把鎖,讓其他線程不能操作該變量;

2、當線程1操作完全局變量 g_num 之後,將該鎖解開,允許其他線程操作該變量;

3、同理,線程2操作 g_num 的時候,也給 g_num 添加一把鎖,不允許其他線程操作;

這樣就能保證,同一時間,只有一個線程可以操作共享資源了。

 

互斥鎖 Lock:

threading 模塊中提供了 Lock 類用於對共享資源的鎖定;

# 導入 threading 模塊的 Thread 類和 Lock 類
from threading import Thread, Lock
import time

# 聲明一個全局變量
g_num = 0;

# 定義兩個方法,分別在兩個方法中對全局變量 g_num 自加 100萬次
def work1():
    global g_num
    for i in range(1000000):
        # 當前線程搶到執行權的時候,對共享資源進行鎖定,
        # 即鎖內的代碼在執行的時候,其他的線程等待,直到解鎖;
        # 參數 True 表示阻塞,即如果對共享資源進行鎖定的時候,
        # 如果資源已經上鎖了,則等待,直到該資源解鎖爲止(默認值);
        # 如果參數爲 False,則表示非阻塞,即不管本次對共享資源
        # 上鎖能不能成功,都不會卡在這裏,而是繼續向下執行;
        mutex.acquire(True)
        g_num += 1
        # 對共享資源進行解鎖
        mutex.release()

    print("work1 ---- g_num:", g_num)

def work2():
    global g_num
    for i in range(1000000):
        mutex.acquire() 	# 上鎖
        g_num += 1
        mutex.release()     # 解鎖
    print("work2 ---- g_num:", g_num)

if __name__ == "__main__":

    # 創建一個互斥鎖,這個鎖默認是沒有上鎖的
    mutex = Lock()

    # 創建子線程,並指定子線程執行的對象
    t1 = Thread(target=work1)
    t1.start()

    t2 = Thread(target=work2)
    t2.start()

上鎖的原則:上面的代碼也可以將鎖加在 for 循環的外面,如下所示:

def work1():
    global g_num
    # 將鎖上在 for 循環外面
    mutex.acquire(True) # 上鎖
    for i in range(1000000):
        g_num += 1
    mutex.release()     # 解鎖
    print("work2 ---- g_num:", g_num)

但是這樣的話,多線程的任務就變成了單線程的任務,每個線程從頭到尾執行完,再執行下一個線程;所以一般來說,上鎖的代碼越少越好,一般只在必須加鎖的位置上鎖,儘量不要把不必要的代碼也放到鎖裏。

鎖的壞處:

1、阻止了多線程併發執行,包含鎖的某段代碼實際上是以單線程模式執行,效率就降低了;

2、由於可以存在多個鎖,不同的線程持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成死鎖;

 

死鎖:

在線程中共享多個資源的時候,如果兩個線程分別佔有一部分資源,並且同時等待對方的資源,就會造成死鎖;下面代碼用來演示死鎖的產生:

# 導入 threading 模塊
import threading
import time

# 自定義一個類,繼承於 Thread 類
class MyThread1(threading.Thread):
    # 重寫 Thread 類中的 run 方法
    def run(self):
        # 先對 mutexA 鎖進行上鎖
        if mutexA.acquire():
            print(self.name+'----do1---up----')
            time.sleep(1)
            # 再對 mutexB 鎖進行上鎖
            if mutexB.acquire():
                print(self.name+'----do1---down----')
                mutexB.release()
            mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        # 先對 mutexB 鎖進行上鎖
        if mutexB.acquire():
            print(self.name+'----do2---up----')
            time.sleep(1)
            # 再對 mutexA 鎖進行上鎖
            if mutexA.acquire():
                print(self.name+'----do2---down----')
                mutexA.release()
            mutexB.release()

if __name__ == '__main__':
    # 創建兩個互斥鎖
    mutexA = threading.Lock()
    mutexB = threading.Lock()

    # 創建兩個子線程
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

輸出結果:

產生死鎖的原理是:

解決死鎖的辦法有:

1、程序設計時儘量避免;

2、添加超時時間:acquire([blocking[, timeout]]) 有兩個參數,blocking 參數在上面介紹互斥鎖的時候已經說了,timeout 參數就是超時時間;如果調用 acquire 方法的時候沒有參數,默認是阻塞的,並且是無休止等待的;如果加了 timeout 參數,比如 acquire(3),就表示只等待 3 秒鐘,3 秒鐘之後不管有沒有上鎖成功,都會繼續向下執行;

 

生產者與消費者模式:

  • 爲什麼要使用生產者與消費者模式:

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

  • 什麼是生產者與消費者模式:

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

Python學習筆記(二十七):進程間通信 Queue 文章中,我們已經知道,多進程之間進行通信,可以通過 multiprocessing 模塊中的 Queue 類來實現;

而多線程之間的通信可以通過 queue 模塊中的 Queue 類來實現(在 python2 中,模塊名爲 Queue);

多線程通信使用的 queue 模塊中的 Queue 類和多進程通信使用的 multiprocessing 模塊中的 Queue 類,具有相同的方法;

使用 Queue 實現生產者與消費者的案例如下:

# 從 queue 模塊中導入 Queue 類
from queue import Queue
# 如果是 python2,模塊名爲 Queue
# from Queue import Queue

# 導入 threading 模塊中的 Thread 類
from threading import Thread
import time

# 生產者類:生產產品
class Producer(Thread):
    # 重寫 Thread 類的 run 方法
    def run(self):
        count = 0
        while True:
            # 如果隊列中的產品數量小於1000,就開始生產
            if queue.qsize() < 1000:
                # 每次生產100個
                for i in range(100):
                    count = count + 1
                    msg = self.name + " 生成產品 " + str(count)
                    queue.put(msg)  # 將生成的產品放入隊列中
                    print(msg)
            else:
                # 如果隊列中產品數量大於1000,生產者就休息一會,
                # 停止生產,等產品被消費到 1000 以下了,再繼續生產
                time.sleep(1)

# 消費者類:消費產品
class Consumer(Thread):
    def run(self):
        while True:
            # 如果隊列中的產品數量大於100,就開始消費
            if queue.qsize() > 100:
                # 每次消費 3 個
                for i in range(3):
                    # 從隊列中取出產品
                    msg = self.name + " 消費了" + queue.get()
                    print(msg)
            else:
                # 如果隊列中產品數量低於100,消費者就休息一會,停止消費,
                # 等隊列中產品數量大於100了,再繼續開始消費;
                time.sleep(1)

if __name__ == "__main__":
    # 創建一個隊列
    queue = Queue()

    # 創建了 500 個初始產品放到隊列中
    for i in range(500):
        queue.put("初始產品:" + str(i))

    # 創建了兩個生產者,生產產品
    for i in range(2):
        p = Producer()
        p.start()

    # 創建了五個消費者,消費產品
    for i in range(5):
        c = Consumer()
        c.start()

 

ThreadLocal:

在多線程環境下,每個線程都有自己的數據,一個線程使用自己獨有的局部變量比使用全局變量好,因爲局部變量只有自己能看見,不會影響其他線程,而全局變量的修改必須加鎖;但是局部變量也有問題,就是在函數調用的時候,傳遞來傳遞去很麻煩;而 ThreadLocal 就是用來解決這類問題的。

ThreadLocal 本身是一個全局變量,但是每個線程都可以用它來保存自己的私有數據,這些私有數據對其他線程是不可見的。ThreadLocal 在每一個線程中都會創建一個副本,每個線程都可以訪問自己內部的副本變量。

import threading

# 創建全局 ThreadLocal 對象
localStudent = threading.local()

def process_student():
    # 獲取當前線程關聯的 studentName;
    # ThreadLocal 中綁定的跟線程有關的數據,對其他線程是不可見的;
    studentName = localStudent.studentName
    print('Hello, %s (in %s)' % (studentName, threading.currentThread().name))

def process_thread(name):
    # 將 studentName 綁定到 localStudent;
    # 每個線程都可以將自己的私有數據綁定到 ThreadLocal;
    localStudent.studentName = name
    process_student()

if __name__ == "__main__":
    # 創建兩個子線程,傳入 name 參數
    t1 = threading.Thread(target=process_thread, args=('jack',), name='Thread-A')
    t1.start()

    t2 = threading.Thread(target=process_thread, args=('lucy',), name='Thread-B')
    t2.start()

 

線程池:

當程序中需要創建大量生存期很短暫的線程時,一個一個的創建,成本是比較高的,因爲每個線程的創建和消亡,都需要消耗時間和資源,此時就可以使用線程池來解決;

線程池在創建時,池中就創建了一定數量空閒的線程,程序只要將一個函數提交給線程池,線程池就會啓動一個空閒的線程來執行它;當該線程執行結束後,該線程並不會死亡,而是再次返回到線程池中,變成空閒狀態;

python3 中可以使用 concurrent.futures 模塊來實現線程池的應用,也可以使用 threadpool 模塊,不過 threadpool 是第三方模塊,需要手動安裝;而且 threadpool 模塊是老版本的,新版本推薦使用 concurrent.futures 模塊,是 python3 自帶的;

線程池的基類是 concurrent.futures 模塊中的 Executor,Executor 提供了兩個子類,即 ThreadPoolExecutor 和 ProcessPoolExecutor,分別用於創建線程池和進程池;

Executor 提供瞭如下方法:

  • submit(fn, *args, **kwargs):將 fn 函數提交給線程池。args 代表傳給 fn 函數的參數,kwargs 代表以關鍵字參數的形式爲 fn 函數傳入參數。

  • map(func, *iterables, timeout=None, chunksize=1):該函數類似於全局函數 map(func, *iterables),只是該函數將會啓動多個線程,以異步方式立即對 iterables 執行 map 處理。

  • shutdown(wait=True):關閉線程池;參數 wait 默認值爲 True,表示等待線程池中的任務全部執行結束,再向下執行;如果爲 False,則不等待;

程序將任務函數提交(submit)給線程池後,submit 方法會返回一個 Future 對象;由於任務函數會在子線程中以異步方式執行,因此,任務函數相當於一個 “將來完成” 的任務,所以 python 使用 Future(未來) 來表示;

Future 類提供瞭如下方法:

  • cancel():取消該 Future 代表的線程任務;如果該任務正在執行,則不可取消,該方法返回 False;否則,程序會取消該任務,並返回 True;

  • cancelled():判斷 Future 代表的線程任務是否被成功取消;如果被成功取消,返回 True;

  • running():判斷 Future 代表的線程是否正在執行;如果正在執行,返回 True;

  • done():判斷 Future 代表的線程是否已經完成;如果已經完成,返回 True;

  • result(timeout=None):獲取 Future 代表的線程最後返回的結果;如果線程任務還未完成,該方法會阻塞當前線程,timeout 參數指定最多阻塞多少秒;

  • exception(timeout=None):獲取 Future 代表的線程返回的異常;如果線程成功完成,沒有異常,返回 True;

  • add_done_callback(fn):爲 Future 代表的線程註冊一個回調函數,當線程執行結束時,會自動觸發回調函數;

在用完一個線程池後,應該調用線程池的 shutdown 方法,該方法將關閉線程池,不再接收新任務,但會將之前提交的所有任務執行完成。

# 導入線程池模塊中的 ThreadPoolExecutor 類和 Future 類
from concurrent.futures import ThreadPoolExecutor, Future
# 導入線程模塊
import threading
import time

# 聲明一個函數,用於在子線程中執行
def work(i):
    print("線程 %s 開始執行 任務%d" %(threading.currentThread().name, i))
    time.sleep(1)

# 回調函數:線程執行結束會自動觸發執行
def callFunc(future):
    print(threading.currentThread().name + " 執行結束")

if __name__ == "__main__":
    # 創建線程池對象,最大線程數爲 3,即線程池中最多存在 3 個子線程
    pool = ThreadPoolExecutor(3)

    # 循環提交 6 個任務給線程池執行
    for i in range(6):
        # 將任務函數提交給線程池,並傳入參數到任務函數中;
        # 如果提交的任務數量大於線程池中的線程數量,那麼
        # 多餘的任務會等待,等待線程池中的線程執行完之前
        # 的任務之後,再來執行等待的新任務;
        # 返回 Future 對象
        future = pool.submit(work, i)
        # 爲線程添加回調函數,默認將 future 對象傳入到回調函數中
        future.add_done_callback(callFunc)

    print("=== start ===")
    # 關閉線程池,不再接收新的任務;
    # 默認參數爲 True,表示等待線程池中的所有任務執行結束後,
    # 再向下執行,下面的 end 會最後輸出;
    # 如果爲 False,則表示不等待,那麼 end 會先輸出;
    pool.shutdown(True)
    print("=== end ===")

輸出結果:

 

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