【目錄】 一、 threading模塊介紹 二 、開啟線程的兩種方式 三 、在一個進程下開啟多個線程與在一個進程下開啟多個子進程的區別 四、 線程相關的其他方法 五、守護線程 六、Python GIL(Global Interpreter Lock) 八、同步鎖 九、死鎖現象與遞歸鎖 十、線程que ...
【目錄】
一、 threading模塊介紹
二 、開啟線程的兩種方式
三 、在一個進程下開啟多個線程與在一個進程下開啟多個子進程的區別
四、 線程相關的其他方法
五、守護線程
六、Python GIL(Global Interpreter Lock)
八、同步鎖
九、死鎖現象與遞歸鎖
十、線程queue
一、 threading模塊介紹
進程的multiprocess模塊,完全模仿了threading模塊的介面,二者在使用層面,有很大的相似性,此處省略好多字。。
multiprocess模塊用法回顧:https://www.cnblogs.com/bigorangecc/p/12759151.html
官網鏈接:https://docs.python.org/3/library/threading.html?highlight=threading#
二 、開啟線程的兩種方式
1、實例化類創建對象
#方式一 from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.start() print('主線程')
2、類的繼承
#方式二 from threading import Thread import time class Sayhi(Thread): def __init__(self,name): super().__init__() self.name=name def run(self): time.sleep(2) print('%s say hello' % self.name) if __name__ == '__main__': t = Sayhi('egon') t.start() print('主線程')
三 、在一個進程下開啟多個線程與在一個進程下開啟多個子進程的區別
1、比較開啟速度
from threading import Thread from multiprocessing import Process import os def work(): print('hello') if __name__ == '__main__': #在主進程下開啟線程 t=Thread(target=work) t.start() print('主線程/主進程') ''' 列印結果: hello 主線程/主進程 ''' #在主進程下開啟子進程 t=Process(target=work) t.start() print('主線程/主進程') ''' 列印結果: 主線程/主進程 hello '''誰的開啟速度快
2、pid
from threading import Thread from multiprocessing import Process import os def work(): print('hello',os.getpid()) if __name__ == '__main__': #part1:在主進程下開啟多個線程,每個線程都跟主進程的pid一樣 t1=Thread(target=work) t2=Thread(target=work) t1.start() t2.start() print('主線程/主進程pid',os.getpid()) #part2:開多個進程,每個進程都有不同的pid p1=Process(target=work) p2=Process(target=work) p1.start() p2.start() print('主線程/主進程pid',os.getpid())瞅一瞅pid
3、同一進程內的線程之間共用進程內的數據
from threading import Thread from multiprocessing import Process import os def work(): global n n=0 if __name__ == '__main__': # n=100 # p=Process(target=work) # p.start() # p.join() # print('主',n) #毫無疑問子進程p已經將自己的全局的n改成了0,但改的僅僅是它自己的,查看父進程的n仍然為100 n=1 t=Thread(target=work) t.start() t.join() print('主',n) #查看結果為0,因為同一進程內的線程之間共用進程內的數據驗證
四、 線程相關的其他方法
1、thread實例對象的方法:
1、isAlive(): 返回線程是否活動的,存活的。
2、getName(): 返回線程名。
3、setName(): 設置線程名。
2、threading模塊提供的一些方法:
1、threading.currentThread()
返回當前的線程變數。
2、threading.enumerate()
返回一個包含正在運行的線程的list。正在運行指線程啟動後、結束前,不包括啟動前和終止後的線程。
3、threading.activeCount()
返回正在運行的線程數量,與 len(threading.enumerate()) 有相同的結果。
from threading import Thread import threading from multiprocessing import Process import os def work(): import time time.sleep(3) print(threading.current_thread().getName()) if __name__ == '__main__': #在主進程下開啟線程 t=Thread(target=work) t.start() print(threading.current_thread().getName()) print(threading.current_thread()) #主線程 print(threading.enumerate()) #連同主線程在內有兩個運行的線程 print(threading.active_count()) print('主線程/主進程') ''' 列印結果: MainThread <_MainThread(MainThread, started 140735268892672)> [<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>] 主線程/主進程 Thread-1 '''應用舉例
3、join()的使用——主線程等待子線程結束
from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.start() t.join() print('主線程') print(t.is_alive()) ''' egon say hello 主線程 False '''慄子
五、守護線程
1、強調
無論是進程還是線程,都遵循:守護xxx會等待主xxx運行完畢後被銷毀
需要強調的是:運行完畢(真正的任務已經做完,但還沒有下班) 並非 終止運行(任務已經完成,收拾好東西下班了)
#1.對主進程來說,運行完畢指的是主進程代碼運行完畢
#2.對主線程來說,運行完畢指的是主線程所在的進程內所有非守護線程統統運行完畢,主線程才算運行完畢
詳細解釋:
#1 主進程在其代碼結束後就已經算運行完畢了(守護進程在此時就被回收),
然後主進程會一直等非守護的子進程都運行完畢後回收子進程的資源(否則會產生僵屍進程),才會結束運行。
#2 主線程在其他非守護線程運行完畢後才算運行完畢(守護線程在此時就被回收)。
因為主線程的結束意味著進程的結束,進程整體的資源都將被回收,而進程必須保證非守護線程都運行完畢後才能結束。
2、兩顆慄子
from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.setDaemon(True) #必須在t.start()之前設置 t.start() print('主線程') print(t.is_alive()) ''' 主線程 True '''慄子1
from threading import Thread import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") t1=Thread(target=foo) t2=Thread(target=bar) t1.daemon=True t1.start() t2.start() print("main-------")慄子2——迷人版
六、Python GIL (Global Interpreter Lock)
1、強調
# 在Cpython解釋器中,同一個進程下開啟的多線程,同一時刻只能有一個線程執行,無法利用多核優勢.
# GIL 並不是python的特性,它是在實現python解釋器(cpython)時所引入的一個概念,
即 GIL 是 python解釋器(cpython)的特性。
2、GIL
GIL 本質就是一把互斥鎖,既然是互斥鎖,所有互斥鎖的本質都一樣,都是將併發運行變成串列,以此來控制同一時間內共用數據只能被一個任務所修改,進而保證數據安全。有了GIL的存在,同一時刻同一進程中只有一個線程被執行。
可以肯定的一點是:保護不同的數據的安全,就應該加不同的鎖。
參考閱讀:
https://www.cnblogs.com/linhaifeng/articles/7449853.html
八、同步鎖
1、三個需要註意的點:
#1.GIL & Lock
線程搶的是GIL鎖,GIL鎖相當於執行許可權,拿到執行許可權後才能拿到互斥鎖Lock,
其他線程也可以搶到GIL,但如果發現Lock仍然沒有被釋放則阻塞,即便是拿到執行許可權GIL也要立刻交出來。
(GIL鎖 和 互斥鎖Lock 都搶到 ,才是王道)
#2. join() & Lock
join是等待所有,即整體串列,而鎖只是鎖住修改共用數據的部分,即部分串列,
要想保證數據安全的根本原理在於讓併發變成串列,join與互斥鎖都可以實現,毫無疑問,互斥鎖的部分串列效率要更高
#3. GIL與互斥鎖的經典分析,見本小節末---必看
2、Python已經有一個GIL來保證同一時間只能有一個線程來執行了,為什麼這裡還需要lock?
一個共識 —— 鎖的目的是為了保護共用的數據,同一時間只能有一個線程來修改共用的數據
一個結論 —— 保護不同的數據就應該加不同的鎖。
明朗答案 —— GIL 與Lock是兩把鎖,保護的數據不一樣,
前者 GIL 是解釋器級別的(當然保護的就是解釋器級別的數據,比如垃圾回收的數據),
後者Lock 是保護用戶自己開發的應用程式的數據,很明顯GIL不負責這件事,只能用戶自定義加鎖處理,即Lock
3、GIL鎖 VS 互斥鎖Lock ——代碼慄子
鎖通常被用來實現對共用資源的同步訪問。
為每一個共用資源創建一個Lock對象,當你需要訪問該資源時,調用acquire方法來獲取鎖對象
(如果其它線程已經獲得了該鎖,則當前線程需等待其被釋放),待資源訪問完後,再調用release方法釋放鎖:
import threading R=threading.Lock() R.acquire() ''' 對公共數據的操作 ''' R.release()
from threading import Thread import os,time def work(): global n temp=n time.sleep(0.1) n=temp-1 if __name__ == '__main__': n=100 l=[] for i in range(100): p=Thread(target=work) l.append(p) p.start() for p in l: p.join() print(n) #結果可能為99join/未加鎖
from threading import Thread,Lock import os,time def work(): global n lock.acquire() temp=n time.sleep(0.1) n=temp-1 lock.release() if __name__ == '__main__': lock=Lock() n=100 l=[] for i in range(100): p=Thread(target=work) l.append(p) p.start() for p in l: p.join() print(n) #結果肯定為0,由原來的併發執行變成串列,犧牲了執行效率保證了數據安全加鎖後
GIL鎖與互斥鎖綜合分析:
#1.100個線程去搶GIL鎖,即搶執行許可權#2. 肯定有一個線程先搶到GIL(暫且稱為線程1),然後開始執行,一旦執行就會拿到lock.acquire()
#3. 極有可能線程1還未運行完畢,就有另外一個線程2搶到GIL,然後開始運行,但線程2發現互斥鎖lock還未被線程1釋放,於是阻塞,被迫交出執行許可權,即釋放GIL
#4.直到線程1重新搶到GIL,開始從上次暫停的位置繼續執行,直到正常釋放互斥鎖lock,然後其他的線程再重覆2 3 4的過程
4、讓併發變成串列——互斥鎖 和 join()的區別
join是等待所有,即整體串列,而鎖只是鎖住修改共用數據的部分,即部分串列,
要想保證數據安全的根本原理在於讓併發變成串列,join與互斥鎖都可以實現,毫無疑問,互斥鎖的部分串列效率要更高
#不加鎖:併發執行,速度快,數據不安全 from threading import current_thread,Thread,Lock import os,time def task(): global n print('%s is running' %current_thread().getName()) temp=n time.sleep(0.5) n=temp-1 if __name__ == '__main__': n=100 lock=Lock() threads=[] start_time=time.time() for i in range(100): t=Thread(target=task) threads.append(t) t.start() for t in threads: t.join() stop_time=time.time() print('主:%s n:%s' %(stop_time-start_time,n)) ''' Thread-1 is running Thread-2 is running ...... Thread-100 is running 主:0.5216062068939209 n:99 ''' #不加鎖:未加鎖部分併發執行,加鎖部分串列執行,速度慢,數據安全 from threading import current_thread,Thread,Lock import os,time def task(): #未加鎖的代碼併發運行 time.sleep(3) print('%s start to run' %current_thread().getName()) global n #加鎖的代碼串列運行 lock.acquire() temp=n time.sleep(0.5) n=temp-1 lock.release() if __name__ == '__main__': n=100 lock=Lock() threads=[] start_time=time.time() for i in range(100): t=Thread(target=task) threads.append(t) t.start() for t in threads: t.join() stop_time=time.time() print('主:%s n:%s' %(stop_time-start_time,n)) ''' Thread-1 is running Thread-2 is running ...... Thread-100 is running 主:53.294203758239746 n:0 ''' #有的同學可能有疑問:既然加鎖會讓運行變成串列,那麼我在start之後立即使用join,就不用加鎖了啊,也是串列的效果啊 #沒錯:在start之後立刻使用jion,肯定會將100個任務的執行變成串列,毫無疑問,最終n的結果也肯定是0,是安全的,但問題是 #start後立即join:任務內的所有代碼都是串列執行的,而加鎖,只是加鎖的部分即修改共用數據的部分是串列的 #單從保證數據安全方面,二者都可以實現,但很明顯是加鎖的效率更高. from threading import current_thread,Thread,Lock import os,time def task(): time.sleep(3) print('%s start to run' %current_thread().getName()) global n temp=n time.sleep(0.5) n=temp-1 if __name__ == '__main__': n=100 lock=Lock() start_time=time.time() for i in range(100): t=Thread(target=task) t.start() t.join() stop_time=time.time() print('主:%s n:%s' %(stop_time-start_time,n)) ''' Thread-1 start to run Thread-2 start to run ...... Thread-100 start to run 主:350.6937336921692 n:0 #耗時是多麼的恐怖 '''互斥鎖與join的區別
九、死鎖現象與遞歸鎖——進程和線程都有死鎖現象與遞歸鎖
所謂死鎖:
是指兩個或兩個以上的進程或線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱為死鎖進程,如下就是死鎖:
from threading import Thread,Lock import time mutexA=Lock() mutexB=Lock() class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A鎖\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B鎖\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B鎖\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A鎖\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start() ''' Thread-1 拿到A鎖 Thread-1 拿到B鎖 Thread-1 拿到B鎖 Thread-2 拿到A鎖 然後就卡住,死鎖了 '''死鎖現象
解決方法——遞歸鎖
在Python中為了支持在同一線程中多次請求同一資源,python提供了可重入鎖RLock。
這個RLock內部維護著一個Lock和一個counter變數,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。上面的例子如果使用RLock代替Lock,則不會發生死鎖:
mutexA=mutexB=threading.RLock() #一個線程拿到鎖,counter加1,該線程內又碰到加鎖的情況,則counter繼續加1,這期間所有其他線程都只能等待,等待該線程釋放所有鎖,即counter遞減到0為止
十、線程 的類 queue
1、線程queue
queue隊列 :使用 import queue,用法與進程Queue一樣
(註意區分——進程的Queue,進程的隊列首字母為大寫Q,線程的為小寫q
進程中的導入方法:from multiprocessing import Process,Queue)
2、主要用法
class queue.
Queue
(maxsize=0) #先進先出
import queue q=queue.Queue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) ''' 結果(先進先出): first second third '''View Code
class queue.
LifoQueue
(maxsize=0) #last in fisrt out
import queue q=queue.LifoQueue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) ''' 結果(後進先出): third second first '''View Code
class queue.
PriorityQueue
(maxsize=0) #存儲數據時可設置優先順序的隊列
import queue q=queue.PriorityQueue() #put進入一個元組,元組的第一個元素是優先順序(通常是數字,也可以是非數字之間的比較),數字越小優先順序越高 q.put((20,'a')) q.put((10,'b')) q.put((30,'c')) print(q.get()) print(q.get()) print(q.get()) ''' 結果(數字越小優先順序越高,優先順序高的優先出隊): (10, 'b') (20, 'a') (30, 'c') '''View Code
參考資料:
https://www.cnblogs.com/linhaifeng/articles/7428877.html