Python 异步编程之——线程
上一篇我們講到,進程是一個相對獨立的單元。而線程則是一個進程內單一順序的控制流,是操作系統運行調度的最小單元。因此,一個進程可以包含多個線程。比如,播放視頻時,畫面和聲音就是不同的線程在處理。
1.創建線程
(1)使用threading.Thread()直接創建
def fun1():print('任務1開始')time.sleep(2)print('任務1結束')def fun2():print('任務2開始')time.sleep(4)print('任務2結束')thread1 = threading.Thread(target=fun1) thread2 = threading.Thread(target=fun2)# 守護線程,隨主線程結束而結束,即不會打印“任務2結束” thread2.setDaemon(True)# 啟動任務 thread1.start() thread2.start()# 主線程會等待本線程完成 thread1.join()(2)繼承Thread
class MyThread(threading.Thread):# 可以給線程取名字def __init__(self, name):super().__init__(name=name)# 需要實現的核心代碼def run(self):print("這里寫核心功能")t = MyThread('線程1') t.start() t.join()2.線程間通訊
同一個進程下的線程,使用的是同一塊內存。因此天然可以進行通信。此外,還可以使用隊列。參看第3部分。
# 定義任意類型的全局變量都可以實現線程間通信 lst = []def fun1():global lstlst.append(1)def fun2():global lstdt = lst.pop()print(dt)thread1 = threading.Thread(target=fun1) thread2 = threading.Thread(target=fun2) thread1.start() thread2.start()3.數據安全和鎖的概念
先直接看一個例子
import threadingdef add():global totalfor i in range(1000000):total += 1def desc():global totalfor i in range(1000000):total -= 1if __name__ == '__main__':total = 0thread1 = threading.Thread(target=add)thread2 = threading.Thread(target=desc)thread1.start()thread2.start()thread1.join()thread2.join()print(total)(1)數據安全
上面代碼表示,有兩個任務,分別由兩個線程完成,定義了一個全局變量total。add函數可以理解為領工資,余額增加;desc函數可以理解為消費,余額減少。循環相同次數,每次變量值相同。可以想見,最終結果應該是0。可當你執行代碼時,你會發現,結果不僅不為0,甚至每次結果都不相同。這樣的結果,不僅不是我們想要的,而且是危險的,因為它變得不可控。而要解釋這個現象,就需要了解程序是怎么執行的。
我們以為的是這樣的:
(2)字節碼
但實際卻不是如此。因為代碼在執行時,會先被編譯為字節碼,下面展示一個簡單函數的字節碼
# 定義一個簡單函數 def add(a):a += 1return a# 查看編譯后的字節碼用dis print(dis.dis(add))"""3 0 LOAD_FAST 0 (a)2 LOAD_CONST 1 (1)4 INPLACE_ADD6 STORE_FAST 0 (a)4 8 LOAD_FAST 0 (a)10 RETURN_VALUE None """?可以看到,一個只包含變量自增功能的函數實際是分步驟完成的。加載變量a—>加載常量1—>執行加法—>保存a。程序在執行時是按照字節碼行數和時間片在不同線程之間跳轉的。不是我們看到的代碼級更不是函數級來切換。因此,實際執行的可能過程示例如下:
?關鍵點就在線程2修改變量這里,線程2在線程1完成修改前就已經加載了變量total,雖然total的值被線程1修改了,但線程2不會再次加載total。因此,最終執行的是0-1=-1。因此,循環次數越大,結果就越不具有確定性。
(3)鎖
原理搞清楚了,但問題還沒解決。而解決線程數據不安全的方法之一就是引入鎖的機制。
從鎖的字面含義就已經表明其解決的方式了-通過鎖來保護變量。舉個生活中的例子,一個變量相當于一個房間,房門有鎖。A拿到鑰匙,進去了,反鎖了門。那么B就只有等著。等A辦完事出來,交出鑰匙,B拿到鑰匙才能進入,B也會把門反鎖。具體代碼實現如下:
"""使用互斥鎖解決數據安全問題""" import threadingdef add():global totalfor i in range(1000000):# 本線程在訪問total這個變量時,其他線程不能訪問lock.acquire()total += 1# 釋放鎖lock.release()def desc():global totalfor i in range(1000000):lock.acquire()total -= 1lock.release()if __name__ == '__main__':total = 0lock = threading.Lock()thread1 = threading.Thread(target=add)thread2 = threading.Thread(target=desc)thread1.start()thread2.start()thread1.join()thread2.join()print(total)鎖不是萬能的,使用鎖會帶來其他問題,比如死鎖。簡單講就是A獲得了鎖,但A需要B的執行結果。可是B還沒有獲得鎖,無法為A提供結果。最終出現相互等待。此外,同一個線程多次請求同一個資源,也會引起死鎖。
(4)使用隊列進行通信
上面的方式很基本,為了更方便實現安全的通信,可以使用queue.Queue(內部實現了鎖),使用方法和多進程的隊列一致。
q = queue.Queue(maxsize=10) q.put(1) dt = q.get() print(dt)4.多線程和多進程
(1)多進程是開啟了多個獨立的單元,相互之間不影響。是真正的并行,同時進行。但是多進程是有代價的。第一,獨立就意味著通信更麻煩;第二,進程與進程的切換是很消耗計算機資源的。
(2)多線程因為是共享內存,因此通信可以很方便。但太方便就意味著可能失控。因此又引入了鎖的機制。實際上python自帶一個大鎖——GIL——全局解釋器鎖。GIL的存在使得同一個時刻,只有一個線程在一個CPU上執行字節碼,且無法將多個線程映射到多個CPU上。實際的執行示意圖如下:
?這樣看來,多線程似乎是沒有意義的。實際上,很多人都評論python的多線程是雞肋。但,實際上。雞肋吃起來還是有味道的。
?線程1發送請求后,會進入漫長的等待。如果這個時候能切換到線程2,即等待的過程我可以做其他事情。那這樣就會比單線程效率更高。
(3)總結
多IO操作的任務使用多線程,多CPU的任務使用多進程。線程的切換代價低于進程,但對于某些任務可能仍然是不可接收的,那是否能在一個線程內完成任務的切換呢?這就是協程的概念了。
總結
以上是生活随笔為你收集整理的Python 异步编程之——线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 叶片 | 风力机叶片设计简化定量分析(一
- 下一篇: android 编译bin文件,Andr