1. 多任務 - 線程
參考
首先考慮一個沒有多任務的程序:
import timedef sing():# 唱歌 5 秒鐘for i in range(5):print("-----菊花臺ing....-----")time.sleep(1)def dance():# 跳舞 5秒鐘for i in range(5):print("-----跳舞.....-----")time.sleep(5)def main():sing()dance()if __name__ == "__main__":main()
此時,你想同時執行唱歌、跳舞是無法做到的。如果想要同時執行,可以使用python提供的Thread來完成.
import time
from threading import Threaddef sing():# 唱歌 5 秒鐘for i in range(5):print("-----菊花臺ing....-----")time.sleep(1)def dance():# 跳舞 5秒鐘for i in range(5):print("-----跳舞.....-----")time.sleep(1)def main():t1 = Thread(target=sing)t2 = Thread(target=dance)t1.start()t2.start()if __name__ == "__main__":main()
關鍵點:
from threading import Thread
: 從threading包鐘導入Threadt1 = Thread(target=sing)
: 使用這個將函數變為線程執行的函數~
1.1 多任務的概念
上面體驗了如何同時執行2個異步函數~下面補充一下多任務的概念.
簡單地說,就是操作系統可以同時運行多個任務.例如: 一邊逛瀏覽器,一遍聽音樂
單核CPU執行多任務: 單核CPU執行多任務的關鍵在于,將cpu的時間切片. 任務1執行0.01秒,然后任務2執行0.01秒,在切到任務1執行0.01秒。由于CPU的運算速度很快,因此,我們感覺就行所有任務都在同時執行一樣
多核CPU執行多任務: 真的并行執行多任務只能在多核CPU上實現. 但是,在平常的代碼中,任務數量會遠遠的大于CPU的核心數,因此操作系統會將任務輪流調度到各個核心上執行~
并行: 同一時刻真正運行在不同的CPU上的任務
并發: 在一個很短的時間內,利用CPU的告訴運轉.執行多個任務
1.2 查看當前線程數量
在某些情況下,需要查看當前程序中的線程數量,可以使用threading.enumerate()
進行嘗試
import threading
from time import sleep, ctimedef sing():for i in range(3):print("正在唱第%d首哥" % i)sleep(2)def dance():for i in range(3):print("正在跳第%d支舞" %i)sleep(2)if __name__ == "__main__":print("開始時間: %s" % ctime())t1 = threading.Thread(target=sing)t2 = threading.Thread(target=dance)t1.start()t2.start()while True:length = threading.enumerate()print("當前的總線程數為: %d" % length)if length <= 1:breaksleep(1)print("結束時間: %s" % ctime())
注意:
- 當調用Thread的時候,不會創建線程
- 當調用Thread創建出來的實例對象的 start方法時,才會創建線程以及讓這個線程開始運行
1.3 創建線程的第二種方法
第一種方法是通過: t = Thread(target = 函數名)
來準備, t.start()
來啟動
第二種方法是通過類繼承threading.Thread來實現,代碼如下:
import threading
import timeclass MyThread(threading.Thread):def run(self):for i in range(3):time.sleep(1)msg = "I`m " + self.name + " @" + str(i)print(msg)if __name__ == "__main__":t = MyThread()t.start()
說明:
- 類的繼承:
class MyThread(threading.Thread)
- 通過類的方式創建的線程,必須在該類中定義run函數. 這樣當調用
t.start()
時候,新創建的線程會去類中尋找run函數并執行 - 一個
t=MyThread()
只會準備一個線程,當t.start()
時,會創建線程~
1.4 線程共享全局變量
有些時候,多個不同的線程可能需要用到同一個變量,下面演示全局變量在多線程中的使用
from threading import Thread
import timedef work1():global g_numfor i in range(3):g_num += 1print("---- in work1, g_num is %d ---" % g_num)def work2():global g_numprint("---- in work2, g_num is %d ---" % g_num)def main():g_num = 100print("---- 線程創建執行之前 g_num is %d ---" % g_num)t1 = Thread(target=work1)t1.start()# 讓 t1線程先執行time.sleep(1)t2 = Thread(target=work2)t2.start()if __name__ == "__main__":main()
注意:
- 在一個函數中對全局變量進行修改的時候需要看是否對全局變量的指向進行了修改
- 如果修改了指向,那么必須使用global
- 如果僅修改了指向中的數據,則可以省略global
1.5 帶參數的線程調用
在調用的時候,可能需要傳遞參數進去.這就需要在線程準備的時候,使用args傳遞參數
from threading import Thread
from time import sleepdef test1(tmp):tmp.append(33)def test2(tmp):tmp.append(66)def main():num_arr = [11, 22]print(str(num_arr))t1 = Thread(target=test1, args=(num_arr,))t2 = Thread(target=test2, args=(num_arr,))t1.start()sleep(1)print(str(num_arr))t2.start()print(str(num_arr))if __name__ == "__main__":main()
注意:
- 多任務共享數據的原因: 多個任務合作同時完成一個大任務~
- 一個任務獲取數據
- 一個任務處理數據
- 一個任務發送數據
1.6 資源競爭
共享變量會產生一個資源競爭的問題: 多個線程同時對一個全局變量進行修改~下面復現問題
import threading
import timeg_num = 0def test1(num):global g_numfor i in range(num):g_num += 1print("test1: g_num: %d" % g_num)def test2(num):global g_numfor i in range(num):g_num += 1print("test2: g_num: %d" % g_num)def main():t1 = threading.Thread(target=test1, args=(1000000,))t2 = threading.Thread(target=test2, args=(1000000,))t1.start()t2.start()time.sleep(5)print("main: g_num: %d" % g_num)if __name__ == "__main__":main()
test2: g_num: 1042014
test1: g_num: 1080242
main: g_num: 1080242
以上的原因如下:
-
假設:
- t1代表線程1,t2代表線程2
- g_num +=1 可分解成下面3個步驟:
- 獲取 g_num的值, 記為t1.1(t2.1)
- 將g_num的值加1, 記為t1.2(t2.2)
- 將加1后的值存入g_num, 記為t1.3(t2.3)
-
下面模擬執行步驟:(根據CPU的特性,分時執行)
- 假設先執行t1.1
- 再執行t1.2
- 然后執行t2.1, 此時重新獲取g_num的值
- 然后執行t1.3, 此時g_num的值并未改變
1.7 同步
以上問題可以通過線程同步來解決,在此之前,需要先了解互斥鎖:
- 當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制
- 互斥鎖未資源引入了一個狀態: 鎖定/非鎖定
- 某個線程要更改共享數據的時候,先將其鎖定,此時資源的狀態為"鎖定",其他線程不能更改;知道該線程釋放資源,將資源的狀態變為"非鎖定",其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性
下面是互斥鎖的基本使用:
# 創建鎖
mutex = threading.Lock()# 鎖定
mutex.acquire()# 釋放
mutex.release()
注意:
- 如果這個鎖之前是沒有上鎖得,那么acquire不會堵塞
- 如果在調用acquire之前已經被上鎖了,那么acquire將會被阻塞直至release釋放
具體做法如下:
import threading
import timedef test1(num, ):global g_num, metexfor i in range(num):metex.acquire()g_num += 1metex.release()print("test1: g_num: %d" % g_num)def test2(num, ):global g_num, metexfor i in range(num):metex.acquire()g_num += 1metex.release()print("test2: g_num: %d" % g_num)g_num = 0
metex = threading.Lock()
def main():# 創建互斥鎖t1 = threading.Thread(target=test1, args=(1000000,))t2 = threading.Thread(target=test2, args=(1000000,))t1.start()t2.start()time.sleep(5)print("main: g_num: %d" % g_num)if __name__ == "__main__":main()
說明:
- 全局變量中創建一個互斥鎖:
metex = threading.Lock()
- 在原子代碼前面添上:
metex.acquire()
- 原子代碼: 即要么一次性全部執行,要么不執行的不可分割的代碼
- 在原子代碼后面添上:
metex.release()
1.8 死鎖
如果兩個線程分別占用一部分資源,并且同時等待對方的資源,就會造成死鎖的現象。下面使用python實現一個簡單的死鎖程序:
import threading
import timeclass MyThread1(threading.Thread):def run(self):# 線程1 假設下面都是原子代碼mutexA.acquire()print(self.name + "do1 up")time.sleep(1)mutexB.acquire()print(self.name + "do1 down")mutexB.release()mutexA.release()class MyThread2(threading.Thread):def run(self):mutexB.acquire()print(self.name + "do2 up")time.sleep(1)mutexA.acquire()print(self.name + "do2 down")mutexA.release()mutexB.release()mutexA = threading.Lock()
mutexB = threading.Lock()def main():t1 = MyThread1()t2 = MyThread2()t1.start()t2.start()if __name__ == "__main__":main()
說明:
- 進入線程1,將mutexA鎖定,然后休眠1秒
- 進入線程2,將mutexB鎖定,然后休眠1秒
- 之后同時在線程1和2中各自獲取mutexB,mutexA而進入相互等待階段,即死鎖。