一、線程介紹 |
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位,一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。
在同一個進程內的線程的數據是可以進行互相訪問的。
線程的切換使用過上下文來實現的,比如有一本書,有a和b這兩個人(兩個線程)看,a看完之后記錄當前看到那一頁哪一行,然后交給b看,b看完之后記錄當前看到了那一頁哪一行,此時a又要看了,那么a就通過上次記錄的值(上下文)直接找到上次看到了哪里,然后繼續往下看。
線程中的5種狀態:
各狀態說明:
1.新建狀態(New):
使用threading.threading創建實例時候,線程還沒有開始運行,此時線程處在新建狀態。 當一個線程處於新生狀態時,程序還沒有開始運行線程中的代碼。
2.就緒狀態(Runnable)
一個新創建的線程並不自動開始運行,要執行線程,必須調用線程的start()方法。當線程對象調用start()方法即啟動了線程,start()方法創建線程運行的系統資源,並調度線程運行run()方法。當start()方法返回后,線程就處於就緒狀態。
處於就緒狀態的線程並不一定立即運行run()方法,線程還必須同其他線程競爭CPU時間,只有獲得CPU時間才可以運行線程。因為在單CPU的計算機系統中,不可能同時運行多個線程,一個時刻僅有一個線程處於運行狀態。因此此時可能有多個線程處於就緒狀態。
3.運行狀態(Running)
當線程獲得CPU時間后,它才進入運行狀態,真正開始執行run()方法.
4. 阻塞狀態(Blocked)
線程運行過程中,可能由於各種原因進入阻塞狀態:
1>線程通過調用sleep方法進入睡眠狀態;
2>線程調用一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返回到它的調用者;
3>線程試圖得到一個鎖,而該鎖正被其他線程持有;
4>線程在等待某個觸發條件;
......
所謂阻塞狀態是正在運行的線程沒有運行結束,暫時讓出CPU,這時其他處於就緒狀態的線程就可以獲得CPU時間,進入運行狀態。
5. 死亡狀態(Dead)
有兩個原因會導致線程死亡:
1) run方法正常退出而自然死亡,
2) 一個未捕獲的異常終止了run方法而使線程猝死。
為了確定線程在當前是否存活着(就是要么是可運行的,要么是被阻塞了),需要使用isAlive方法。如果是可運行或被阻塞,這個方法返回true,如果線程仍舊是new狀態且不是可運行的, 或者線程死亡了,則返回false.
python中的多線程:
Python通過兩個標准庫thread和threading提供對線程的支持。thread提供了低級別的、原始的線程以及一個簡單的鎖,threading則彌補了其缺陷,所以線程模塊使用threading就可以了。
多線程在Python內實則就是一個假象,為什么這么說呢,因為CPU的處理速度是很快的,所以我們看起來以一個線程在執行多個任務,每個任務的執行速度是非常之快的,利用上下文切換來快速的切換任務,以至於我們根本感覺不到。
但是頻繁的使用上下文切換也是要耗費一定的資源,因為單線程在每次切換任務的時候需要保存當前任務的上下文。
什么時候用到多線程?
首先IO操作是不占用CPU的,只有計算的時候才會占用CPU(譬如1+1=2),Python中的多線程不適合CPU密集型的任務,適合IO密集型的任務(sockt server).
IO密集型(I/O bound):頻繁網絡傳輸、讀取硬盤及其他IO設備稱之為IO密集型,最簡單的就是硬盤存取數據,IO操作並不會涉及到CPU。
計算密集型(CPU bound):程序大部分在做計算、邏輯判斷、循環導致cpu占用率很高的情況,稱之為計算密集型,比如說python程序中執行了一段代碼1+1
,這就是在計算1+1的值
線程創建方法:
1.普通方法
#!/usr/bin/env python3 #_*_ coding:utf-8 _*_ #Author:wd import threading def task(n): print('run task ',n) for i in range(50):#啟動50個線程 t=threading.Thread(target=task,args=(i,))#args參數是一個tuple t.start()#啟動線程
2.類繼承方法
#!/usr/bin/env python3 #_*_ coding:utf-8 _*_ #Author:wd import threading class Mythreading(threading.Thread): def __init__(self,fun,args): self._fun=fun self._agrs=args super(Mythreading,self).__init__() def run(self):#啟動線程時候會允許run方法,這里重寫父類run方法,可以自定義需要運行的task print('start running ....') self._fun(self._agrs) def func(n): print(n) t=Mythreading(func,1) t.start()#啟動運行線程 結果: start running .... 1
threading模塊提供方法:
- start 線程准備就緒,等待CPU調度
- setName 為線程設置名稱
- getName 獲取線程名稱
- setDaemon 設置為守護線程(在start之前),默認為前台線程,設置為守護線程以后,如果主線程退出,守護線程無論執行完畢都會退出
- join 等待線程執行結果,逐個執行每個線程,執行完畢后繼續往下執行,該方法使得多線程變得無意義
- run 線程被cpu調度后自動執行線程對象的run方法
- isAlive 判斷線程是否活躍
- threading.active_count 返回當前活躍的線程數量
- threading.current_thread 獲取當前線程對象
使用list主線程阻塞,子現在並行執行demo:
#!/usr/bin/env python3 #_*_ coding:utf-8 _*_ #Author:wd import threading import time def fun(n): print('start running',n) time.sleep(2) print('end runing ',n) thread_list=[] for i in range(10): t=threading.Thread(target=fun,args=(i,)) t.start() thread_list.append(t) for r in thread_list:#循環每個線程,等待其結果,好處是,這樣做所有線程啟動之后一起join r.join()
這樣的好處在於,在啟動線程后統一join,縮短了程序運行時間,並且提高運行效率。
關於python的GIL(Global Interpreter Lock)
首先需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就好比C++是一套語言(語法)標准,但是可以用不同的編譯器來編譯成可執行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。Python也一樣,同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行。像其中的JPython就沒有GIL。然而因為CPython是大部分環境下默認的Python執行環境。所以在很多人的概念里CPython就是Python,也就想當然的把GIL歸結為Python語言的缺陷。所以這里要先明確一點:GIL並不是Python的特性,Python完全可以不依賴於GIL。
Python GIL其實是功能和性能之間權衡后的產物,它尤其存在的合理性,也有較難改變的客觀因素,無論你啟多少個線程,你有多少個cpu, Python在執行的時候在同一時刻只允許一個線程運行。
線程鎖(互斥鎖Mutex)
一個進程下可以啟動多個線程,多個線程共享父進程的內存空間,也就意味着每個線程可以訪問同一份數據,此時,如果2個線程同時要修改同一份數據,會出現什么狀況?
import threading import time NUM=5 def fun(): global NUM NUM-=1 time.sleep(2) print(NUM) for i in range(5): t=threading.Thread(target=fun) t.start() 結果: 0 0 0 0 0
上述結果並不是我們想要的,去掉了sleep結果才是我們想要的,若是不去掉sleep呢,該怎么辦?,此時我們可以加鎖實現。
import threading import time NUM=5#共享數據 def fun(): global NUM lock.acquire()#獲取鎖 NUM-=1 time.sleep(2) print(NUM) lock.release()#釋放鎖 lock=threading.Lock() for i in range(5): t=threading.Thread(target=fun) t.start()
RLock(遞歸鎖)
遞歸鎖,通俗來講就是大鎖里面再加小鎖,有人可能會問,那我使用Lock不就完了嗎,其實不然,想象一下,現在有兩道門,一把鎖對應一把鑰匙,如果使用Lock,進去第一個門獲取一把鎖,在進去第二個人門又獲取一把鎖,然后要出來開鎖時候(釋放鎖)程序還是用第一個門進來的鑰匙,此時就會一直阻塞,那么RLock就解決了這樣的問題場景。
demo
import threading import time l=threading.RLock()#實例化 def indoor(name): print('%s across second door'%name) l.acquire() print('%s do somethind',name) time.sleep(2) l.release() def outdoor(name): print('%s across first door'%name) l.acquire() indoor(name) print('%s do somethind'% name) time.sleep(1) l.release() outdoor('wd') #上述代碼,若將l=threading.RLock()改為l=threading.Lock(),程序將一直阻塞。
Semaphore&BoundedSemaphore(信號量)
前面已經介紹過了互斥鎖, 互斥鎖同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據 ,比如廁所有3個坑,那最多只允許3個人上廁所,后面的人只能等里面有人出來了才能再進去.
1.Semaphore和BoundedSemaphore使用方法一致
方法:
- acquire(blocking=True,timeout=None)
- release()
demo:
import threading import time def door(n): sp.acquire()#獲取一把鎖,可設置超時時間 print('%d in the door'% n) time.sleep(1) print('%d out the door'% n) sp.release()#釋放鎖 sp=threading.Semaphore(3)#最多允許3個線程同時運行(獲取到信號量) for i in range(5): t=threading.Thread(target=door,args=(i,)) t.start()#啟動線程 結果: 0 in the door 1 in the door 2 in the door 1 out the door 0 out the door 3 in the door 4 in the door 2 out the door 4 out the door 3 out the door
Events
Event是線程間通信最間的機制之一:一個線程發送一個event信號,其他的線程則等待這個信號。用於主線程控制其他線程的執行。 Events 維護着一個flag,這個flag可以使用set()設置成True或者使用clear()重置為False,flag默認為False,而當flag為false時候,wait(timeout=s)則阻塞。
常用方法:
- Event.set():將標志設置為True
- Event.clear():清空標志位,設置為False
- Event.wait(timeout=s):等待(阻塞),直到標志位變成True
- Event.is_set():判斷標志位是否被設置
通過Event來實現兩個或多個線程間的交互,例如紅綠燈,即起動一個線程做交通指揮燈,生成幾個線程做車輛,車輛行駛按紅燈停,綠燈行的規則。
demo:
import threading,time import random def light(): if not event.isSet(): event.set() #wait就不阻塞 #綠燈狀態 count = 0 while True: if count < 10: print('\033[42;1m--green light on---\033[0m') elif count <13: print('\033[43;1m--yellow light on---\033[0m') elif count <20: if event.isSet(): event.clear() print('\033[41;1m--red light on---\033[0m') else: count = 0 event.set() #打開綠燈 time.sleep(1) count +=1 def car(n): while 1: time.sleep(random.randrange(10)) if event.isSet(): #綠燈 print("car [%s] is running.." % n) else: print("car [%s] is waiting for the red light.." %n) #event.wait() if __name__ == '__main__': event = threading.Event() Light = threading.Thread(target=light) Light.start() for i in range(3): t = threading.Thread(target=car,args=(i,)) t.start()
Timer(定時器)
Timer用來定時執行某個線程,若取消運行,則使用cancel方法。
demo:
import threading def show_name(name): print("my name is ",name) t=threading.Timer(5,show_name,args=('wd',))#設置5秒后運行該線程 t.start()#啟動線程 print('要開始運行線程了') t.cancel()#取消運行的線程 結果: 要開始運行線程了
線程池:
啟動一個線程消耗的資源非常少,所以對線程的使用官方並沒有給出標准的線程池模塊,第三方模塊(Threadpool),下面我們自己定義簡單線程池。
簡單demo:
import threading import queue import time class MyThread: def __init__(self,max_num=10): self.queue = queue.Queue() for n in range(max_num): self.queue.put(threading.Thread) def get_thread(self): return self.queue.get() def put_thread(self): self.queue.put(threading.Thread) pool = MyThread(5) def RunThread(arg,pool): print(arg) time.sleep(2) pool.put_thread() for n in range(10): Thread=pool.get_thread() t=Thread(target=RunThread, args=(n,pool,)) t.start()
二、進程介紹 |
一個進程至少要包含一個線程,每個進程在啟動的時候就會自動的啟動一個線程,進程里面的第一個線程就是主線程,每次在進程內創建的子線程都是由主線程進程創建和銷毀,子線程也可以由主線程創建出來的線程創建和銷毀線程。
進程是對各種資源管理的集合,比如要調用內存、CPU、網卡、聲卡等,進程要操作上述的硬件之前都必須要創建一個線程,進程里面可以包含多個線程,QQ就是一個進程。
繼續拿QQ來說,比如我現在打卡了QQ的聊天窗口、個人信息窗口、設置窗口等,那么每一個打開的窗口都是一個線程,他們都在執行不同的任務,比如聊天窗口這個線程可以和好友進行互動,聊天,視頻等,個人信息窗口我可以查看、修改自己的資料。
為了進程安全起見,所以兩個進程之間的數據是不能夠互相訪問的(默認情況下),比如自己寫了一個應用程序,然后讓別人運行起來,那么我的這個程序就可以訪問用戶啟動的其他應用,我可以通過我自己的程序去訪問QQ,然后拿到一些聊天記錄等比較隱秘的信息,那么這個時候就不安全了,所以說進程與進程之間的數據是不可以互相訪問的,而且每一個進程的內存是獨立的。
多進程
多進程的資源是獨立的,不可以互相訪問,如果想多個進程之間實現數據交互就必須通過中間件實現。
啟動一個進程方法與啟動一個線程類似
demo:
#!/usr/bin/env python3 #_*_ coding:utf-8 _*_ #Author:wd from multiprocessing import Process def show(n): print('runing',n) if __name__ == '__main__': p=Process(target=show,args=(1,)) p.start() p.join()
在進程中啟動線程:
#!/usr/bin/env python3 #_*_ coding:utf-8 _*_ #Author:wd from multiprocessing import Process import threading import os def fun(n): print("run threading ",n) print('當前進程id ',os.getpid()) def show(n): print("子進程id:{} 父進程id:{} ".format(os.getpid(),os.getppid()))#ppid指父進程pid,pid當前進程pid print('runing',n) t=threading.Thread(target=fun,args=(2,))#啟動一個線程 t.start() t.join() if __name__ == '__main__': print("主進程id",os.getpid()) p=Process(target=show,args=(1,)) p.start() p.join() 結果: 主進程id 8092 子進程id:9100父進程id:8092 runing 1 run threading 2 當前進程id 9100
進程間通信方法(Queue、Pipes、Mangers)
前面已經提到,進程間通信是需要中間件來實現的,下面介紹幾個實現進程間通信的中間件。
1.進程Queue:建立一個共享的隊列(其實並不是共享的,實際是克隆的,內部維護着數據的共享),多個進程可以向隊列里存/取數據。
demo:
#!/usr/bin/env python3 #_*_ coding:utf-8 _*_ #Author:wd from multiprocessing import Queue,Process def fun(q): q.put([1,2,3]) if __name__ == '__main__': Q=Queue(5)#設置進程隊列長度 for i in range(2):#啟動兩個進程,想隊列里put數據 process=Process(target=fun,args=(Q,))#創建一個進程,將Q傳入,實際上是克隆了Q process.start() process.join() print(Q.get())#在主進程中獲取元素 print(Q.get()) 結果: [1, 2, 3] [1, 2, 3]
2.Pipes(管道)
正如其名,進程間的管道內部機制通過啟動socket連接來維護兩個進程間的通訊。
demo:
from multiprocessing import Process,Pipe def son_process(con): con.send('hello wd')#向管道另一頭發起 print("from father:",con.recv())#子進程收取數據,如果沒收到會阻塞 if __name__ == '__main__': son,father=Pipe()#實例化管道,生成socket連接,一個客戶端一個服務端 P=Process(target=son_process,args=(son,))#啟動一個子進程,將生成的socket對象傳遞進去 P.start() #P.join() print(father.recv())#主進程收取子進程信息 father.send('ok,i know') 結果: hello wd from father: ok,i know
3.Manager(數據共享)
Manager實現了多個進程間的數據共享,支持的數據類型有 list
, dict
, Namespace
, Lock
, RLock
, Semaphore
, BoundedSemaphore
, Condition
, Event
, Barrier
, Queue
, Value
and Array。
demo:
from multiprocessing import Manager,Process def fun(l,d): l.append(1) l.append(2) d['name']='wd' if __name__ == '__main__': with Manager() as manager: L=manager.list()#定義共享列表 D=manager.dict()#定義共享字典 p1=Process(target=fun,args=(L,D))#啟動一個進程,將定義的list和dict傳入 p1.start()#啟動進程 p1.join()#等帶結果 print(L) print(D) 結果: [1, 2] {'name': 'wd'}
進程鎖(Lock)
進程鎖和線程鎖使用語法上完全一致。
demo:
from multiprocessing import Process,Lock import time def fun(l,i): l.acquire()#獲取鎖 print("running process ",i) time.sleep(2) l.release()#釋放 if __name__ == '__main__': lock=Lock()#生成鎖的實例 for i in range(5): p=Process(target=fun,args=(lock,i))#創建進程 p.start()#啟動,這里沒有join,看到的效果還是竄行的 結果: running process 0 running process 1 running process 2 running process 3 running process 4
pool(進程池)
進程池內部維護一個進程序列,當使用時,則去進程池中獲取一個進程,如果進程池序列中沒有可供使用的進進程,那么程序就會等待,直到進程池中有可用進程為止。
主要方法:
- apply :該方法啟動進程為串行執行
- apply_async:啟動進程為並行執行
demo:
from multiprocessing import Pool import os import time def fun(i): print('runing process ',i) time.sleep(2) def end_fun(i=None): print('done') print("call back process:",os.getpid()) if __name__ == '__main__': print("主進程:",os.getpid()) pool=Pool(3)#最多運行3個進程同時運行 for i in range(5): pool.apply_async(func=fun,args=(i,),callback=end_fun)#並行執行,callback回調由主程序回調 #pool.apply(func=fun,args=(i,))串行執行 pool.close()#關閉進程池 pool.join()#進程池中進程執行完畢后再關閉,如果注釋,那么程序直接關閉 結果: 主進程: 12396 runing process 0 runing process 1 runing process 2 runing process 3 runing process 4 done call back process: 12396 done call back process: 12396 done call back process: 12396 done call back process: 12396 done call back process: 12396
三、線程與進程的關系與區別 |
- 線程是執行的指令集,進程是資源的集合;
- 線程的啟動速度要比進程的啟動速度要快;
- 兩個線程的執行速度是一樣的;
- 進程與線程的運行速度是沒有可比性的;
- 線程共享創建它的進程的內存空間,進程的內存是獨立的。
- 兩個線程共享的數據都是同一份數據,兩個子進程的數據不是共享的,而且數據是獨立的;
- 同一個進程的線程之間可以直接交流,同一個主進程的多個子進程之間是不可以進行交流,如果兩個進程之間需要通信,就必須要通過一個中間代理來實現;
- 一個新的線程很容易被創建,一個新的進程創建需要對父進程進行一次克隆
- 一個線程可以控制和操作同一個進程里的其他線程,線程與線程之間沒有隸屬關系,但是進程只能操作子進程
- 改變主線程,有可能會影響到其他線程的行為,但是對於父進程的修改是不會影響子進程;