1 概念梳理:
1.1 線程
1.1.1 什麼是線程
線程是作業系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流程,一個進程中可以並發多個線程,每條線程並存執行不同的任務。一個線程是一個execution context(執行內容),即一個cpu執行時所需要的一串指令。
1.1.2 線程的工作方式
假設你正在讀一本書,沒有讀完,你想休息一下,但是你想在回來時恢複到當時讀的具體進度。有一個方法就是記下頁數、行數與字數這三個數值,這些數值就是execution context。如果你的室友在你休息的時候,使用相同的方法讀這本書。你和她只需要這三個數字記下來就可以在交替的時間共同閱讀這本書了。
線程的工作方式與此類似。CPU會給你一個在同一時間能夠做多個運算的幻覺,實際上它在每個運算上只花了極少的時間,本質上CPU同一時刻只幹了一件事。它能這樣做就是因為它有每個運算的execution context。就像你能夠和你朋友共用同一本書一樣,多任務也能共用同一塊CPU。
1.2 進程
一個程式的執行執行個體就是一個進程。每一個進程提供執行程式所需的所有資源。(進程本質上是資源的集合)
一個進程有一個虛擬地址空間、可執行檔代碼、作業系統的介面、安全的上下文(記錄啟動該進程的使用者和許可權等等)、唯一的進程ID、環境變數、優先順序類、最小和最大的工作空間(記憶體空間),還要有至少一個線程。
每一個進程啟動時都會最先產生一個線程,即主線程。然後主線程會再建立其他的子線程。
與進程相關的資源套件括:
記憶體頁(同一個進程中的所有線程共用同一個記憶體空間)
檔案描述符(e.g. open sockets)
安全憑證(e.g.啟動該進程的使用者ID)
1.3 進程與線程區別
1.同一個進程中的線程共用同一記憶體空間,但是進程之間是獨立的。
2.同一個進程中的所有線程的資料是共用的(進程通訊),進程之間的資料是獨立的。
3.對主線程的修改可能會影響其他線程的行為,但是父進程的修改(除了刪除以外)不會影響其他子進程。
4.線程是一個內容相關的執行指令,而進程則是與運算相關的一簇資源。
5.同一個進程的線程之間可以直接通訊,但是進程之間的交流需要藉助中間代理來實現。
6.建立新的線程很容易,但是建立新的進程需要對父進程做一次複製。
7.一個線程可以操作同一進程的其他線程,但是進程只能操作其子進程。
8.線程啟動速度快,進程啟動速度慢(但是兩者運行速度沒有可比性)。
2 多線程
2.1 線程常用方法
方法 注釋
start() 線程準備就緒,等待CPU調度
setName() 為線程設定名稱
getName() 擷取線程名稱
setDaemon(True) 設定為守護線程
join() 逐個執行每個線程,執行完畢後繼續往下執行
run() 線程被cpu調度後自動執行線程對象的run方法,如果想自訂線程類,直接重寫run方法就行了
2.1.1 Thread類
1.普通建立方式
import threading
import time
def run(n):
print(“task”, n)
time.sleep(1)
print(‘2s’)
time.sleep(1)
print(‘1s’)
time.sleep(1)
print(‘0s’)
time.sleep(1)
t1 = threading.Thread(target=run, args=(“t1”,))
t2 = threading.Thread(target=run, args=(“t2”,))
t1.start()
t2.start()
“””
task t1
task t2
2s
2s
1s
1s
0s
0s
“””
2.繼承threading.Thread來自訂線程類
其本質是重構Thread類中的run方法
import threading
import time
class MyThread(threading.Thread):
def init(self, n):
super(MyThread, self).init() # 重構run函數必須要寫
self.n = n
def run(self): print("task", self.n) time.sleep(1) print('2s') time.sleep(1) print('1s') time.sleep(1) print('0s') time.sleep(1)
if name == “main“:
t1 = MyThread(“t1”)
t2 = MyThread(“t2”)
t1.start()t2.start()
2.1.2 計運算元線程執行的時間
註:sleep的時候是不會佔用cpu的,在sleep的時候作業系統會把線程暫時掛起。
join() #等此線程執行完後,再執行其他線程或主線程
threading.current_thread() #輸出當前線程
import threading
import time
def run(n):
print(“task”, n,threading.current_thread()) #輸出當前的線程
time.sleep(1)
print(‘3s’)
time.sleep(1)
print(‘2s’)
time.sleep(1)
print(‘1s’)
strat_time = time.time()
t_obj = [] #定義列表用於存放子線程執行個體
for i in range(3):
t = threading.Thread(target=run, args=(“t-%s” % i,))
t.start()
t_obj.append(t)
“””
由主線程產生的三個子線程
task t-0 實測:在python2.7、mac os下,運行以下代碼可能會產生髒資料。但是在python3中就不一定會出現下面的問題。
import threading
import time
def run(n):
global num
num += 1
num = 0
t_obj = []
for i in range(20000):
t = threading.Thread(target=run, args=(“t-%s” % i,))
t.start()
t_obj.append(t)
for t in t_obj:
t.join()
print “num:”, num
“””
產生髒資料後的運行結果:
num: 19999
“””
2.5 互斥鎖(mutex)
為了方式上面情況的發生,就出現了互斥鎖(Lock)
import threading
import time
def run(n):
lock.acquire() #擷取鎖
global num
num += 1
lock.release() #釋放鎖
lock = threading.Lock() #執行個體化一個鎖對象
num = 0
t_obj = []
for i in range(20000):
t = threading.Thread(target=run, args=(“t-%s” % i,))
t.start()
t_obj.append(t)
for t in t_obj:
t.join()
print “num:”, num
2.6 遞迴鎖
RLcok類的用法和Lock類一模一樣,但它支援嵌套,,在多個鎖沒有釋放的時候一般會使用使用RLcok類。
import threading
import time
gl_num = 0
lock = threading.RLock()
def Func():
lock.acquire()
global gl_num
gl_num +=1
time.sleep(1)
print gl_num
lock.release()
for i in range(10):
t = threading.Thread(target=Func)
t.start()
2.7 訊號量(BoundedSemaphore類)
互斥鎖同時只允許一個線程更改資料,而Semaphore是同時允許一定數量的線程更改資料 ,比如廁所有3個坑,那最多隻允許3個人上廁所,後面的人只能等裡面有人出來了才能再進去。
import threading
import time
def run(n):
semaphore.acquire() #加鎖
time.sleep(1)
print(“run the thread:%s\n” % n)
semaphore.release() #釋放
num = 0
semaphore = threading.BoundedSemaphore(5) # 最多允許5個線程同時運行
for i in range(22):
t = threading.Thread(target=run, args=(“t-%s” % i,))
t.start()
while threading.active_count() != 1:
pass # print threading.active_count()
else:
print(‘—–all threads done—–’)
2.8 事件(Event類)
python線程的事件用於主線程式控制制其他線程的執行,事件是一個簡單的線程同步對象,其主要提供以下幾個方法:
方法 注釋
clear 將flag設定為“False”
set 將flag設定為“True”
is_set 判斷是否設定了flag
wait 會一直監聽flag,如果沒有檢測到flag就一直處於阻塞狀態
事件處理的機制:全域定義了一個“Flag”,當flag值為“False”,那麼event.wait()就會阻塞,當flag值為“True”,那麼event.wait()便不再阻塞。 利用Event類類比紅綠燈
import threading
import time
event = threading.Event()
def lighter():
count = 0
event.set() #初始值為綠燈
while True:
if 5 < count <=10 :
event.clear() # 紅燈,清除標誌位
print(“\33[41;1mred light is on…\033[0m”)
elif count > 10:
event.set() # 綠燈,設定標誌位
count = 0
else:
print(“\33[42;1mgreen light is on…\033[0m”)
time.sleep(1) count += 1
def car(name):
while True:
if event.is_set(): #判斷是否設定了標誌位
print(“[%s] running…”%name)
time.sleep(1)
else:
print(“[%s] sees red light,waiting…”%name)
event.wait()
print(“[%s] green light is on,start going…”%name)
light = threading.Thread(target=lighter,)
light.start()
car = threading.Thread(target=car,args=(“MINI”,))
car.start()
2.9 條件(Condition類)
使得線程等待,只有滿足某條件時,才釋放n個線程
2.10 定時器(Timer類)
定時器,指定n秒後執行某操作
from threading import Timer
def hello():
print(“hello, world”)
t = Timer(1, hello)
t.start() # after 1 seconds, “hello, world” will be printed
3 多進程
在linux中,每個進程都是由父進程提供的。每啟動一個子進程就從父進程複製一份資料,但是進程之間的資料本身是不能共用的。
from multiprocessing import Process
import time
def f(name):
time.sleep(2)
print(‘hello’, name)
if name == ‘main‘:
p = Process(target=f, args=(‘bob’,))
p.start()
p.join()
from multiprocessing import Process
import os
def info(title):
print(title)
print(‘module name:’, name)
print(‘parent process:’, os.getppid()) #擷取父進程id
print(‘process id:’, os.getpid()) #擷取自己的進程id
print(“\n\n”)
def f(name):
info(‘\033[31;1mfunction f\033[0m’)
print(‘hello’, name)
if name == ‘main‘:
info(‘\033[32;1mmain process line\033[0m’)
p = Process(target=f, args=(‘bob’,))
p.start()
p.join()
3.1 處理序間通訊
由於進程之間資料是不共用的,所以不會出現多線程GIL帶來的問題。多進程之間的通訊通過Queue()或Pipe()來實現
3.1.1 Queue()
使用方法跟threading裡的queue差不多
from multiprocessing import Process, Queue
def f(q):
q.put([42, None, ‘hello’])
if name == ‘main‘:
q = Queue()
p = Process(target=f, args=(q,))
p.start()
print(q.get()) # prints “[42, None, ‘hello’]”
p.join()
3.1.2 Pipe()
Pipe的本質是進程之間的資料傳遞,而不是資料共用,這和socket有點像。pipe()返回兩個連線物件分別表示管道的兩端,每端都有send()和recv()方法。如果兩個進程試圖在同一時間的同一端進行讀取和寫入那麼,這可能會損壞管道中的資料。
from multiprocessing import Process, Pipe
def f(conn):
conn.send([42, None, ‘hello’])
conn.close()
if name == ‘main‘:
parent_conn, child_conn = Pipe()
p = Process(target=f, args=(child_conn,))
p.start()
print(parent_conn.recv()) # prints “[42, None, ‘hello’]”
p.join()
3.2 Manager
通過Manager可實現進程間資料的共用。Manager()返回的manager對象會通過一個服務進程,來使其他進程通過代理的方式操作python對象。manager對象支援 list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value ,Array.
from multiprocessing import Process, Manager
def f(d, l):
d[1] = ‘1’
d[‘2’] = 2
d[0.25] = None
l.append(1)
print(l)
if name == ‘main‘:
with Manager() as manager:
d = manager.dict()
l = manager.list(range(5)) p_list = [] for i in range(10): p = Process(target=f, args=(d, l)) p.start() p_list.append(p) for res in p_list: res.join() print(d) print(l)
3.3 進程鎖(進程同步)
資料輸出的時候保證不同進程的輸出內容在同一塊螢幕正常顯示,防止資料亂序的情況。
Without using the lock output from the different processes is liable to get all mixed up.
from multiprocessing import Process, Lock
def f(l, i):
l.acquire()
try:
print(‘hello world’, i)
finally:
l.release()
if name == ‘main‘:
lock = Lock()
for num in range(10): Process(target=f, args=(lock, num)).start()
3.4 進程池
由於進程啟動的開銷比較大,使用多進程的時候會導致大量記憶體空間被消耗。為了防止這種情況發生可以使用進程池,(由於啟動線程的開銷比較小,所以不需要線程池這種概念,多線程只會頻繁得切換cpu導致系統變慢,並不會佔用過多的記憶體空間)
進程池中常用方法:
apply() 同步執行(串列)
apply_async() 非同步執行(並行)
terminate() 立刻關閉進程池
join() 主進程等待所有子進程執行完畢。必須在close或terminate()之後。
close() 等待所有進程結束後,才關閉進程池。
from multiprocessing import Process,Pool
import time
def Foo(i):
time.sleep(2)
return i+100
def Bar(arg):
print(‘–>exec done:’,arg)
pool = Pool(5) #允許進程池同時放入5個進程
for i in range(10):
pool.apply_async(func=Foo, args=(i,),callback=Bar) #func子進程執行完後,才會執行callback,否則callback不執行(而且callback是由父進程來執行了)
#pool.apply(func=Foo, args=(i,))
print(‘end’)
pool.close()
pool.join() #主進程等待所有子進程執行完畢。必須在close()或terminate()之後。
進程池內部維護一個進程式列,當使用時,去進程池中擷取一個進程,如果進程池序列中沒有可供使用的進程,那麼程式就會等待,直到進程池中有可用進程為止。在上面的程式中產生了10個進程,但是只能有5同時被放入進程池,剩下的都被暫時掛起,並不佔用記憶體空間,等前面的五個進程執行完後,再執行剩下5個進程。
4 補充:協程
線程和進程的操作是由程式觸發系統介面,最後的執行者是系統,它本質上是作業系統提供的功能。而協程的操作則是程式員指定的,在python中通過yield,人為的實現並發處理。
協程存在的意義:對於多線程應用,CPU通過切片的方式來切換線程間的執行,線程切換時需要耗時。協程,則只使用一個線程,分解一個線程成為多個“微線程”,在一個線程中規定某個代碼塊的執行順序。
協程的適用情境:當程式中存在大量不需要CPU的操作時(IO)。
常用第三方模組gevent和greenlet。(本質上,gevent是對greenlet的進階封裝,因此一般用它就行,這是一個相當高效的模組。)
4.1 greenlet
from greenlet import greenlet
def test1():
print(12)
gr2.switch()
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch()
print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
實際上,greenlet就是通過switch方法在不同的任務之間進行切換。
4.2 gevent
from gevent import monkey; monkey.patch_all()
import gevent
import requests
def f(url):
print(‘GET: %s’ % url)
resp = requests.get(url)
data = resp.text
print(‘%d bytes received from %s.’ % (len(data), url))
gevent.joinall([
gevent.spawn(f, ‘https://www.python.org/‘),
gevent.spawn(f, ‘https://www.yahoo.com/‘),
gevent.spawn(f, ‘https://github.com/‘),
])
通過joinall將任務f和它的參數進行統一調度,實現單線程中的協程。代碼封裝層次很高,實際使用只需要瞭解它的幾個主要方法即可。