進程線程
系統知識
計算機系統抽象組成: CPU + 存儲器 + IO
- 資源:
- 計算資源:cpu
- 存儲資源:內存、磁盤等
多任務
• 你的計算機同一時刻能做幾件事?
• 查看任務管理器
• 思考:
• 對於單核的cpu能不能同時執行多任務?
• 能不能真正實現多任務同時執行?
• cpu時間片(抽象概念)
• 對於單核cpu同一時刻只能有一個任務運行。
- 併發:
交替執行(某時間段內的處理能力)
- 並行:
同時執行
• 線程:
線程是操作系統最小的調度單位, 是一串指令的集合
• 進程:
進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位
進程與線程
• 真正在cpu上運行的是線程
• 線程共享
內存空間;進程的內存是獨立
的
• 一個線程只能屬於一個進程,而一個進程可以有多個線程,
但至少有一個線程
• 資源分配給進程,同一進程的所有線程共享該進程的所有資
源。進程的資源是獨立的
• 同一個進程的線程之間可以直接交流;兩個進程想通信,必
須通過一個中間代理
來實現
多任務操作系統工作模式
• 多進程模式
• 多線程模式
• 多進程+多線程模式
進程狀態模型
多進程與多線程
threading
io等待時間較長適合多線程
線程
• 線程被稱爲輕量級進程(Lightweight Process,LWP),是cpu調度的基本單位
• 組成:線程ID、當前指令指針(PC)
、寄存器集合、堆棧
組成
• 在單個程序中同時運行多個線程完成不同的工作,稱爲多線程。
功能
• threading
用於提供線程相關的操作,線程是應用程序中工作的最小單元。
threading模塊提供的常用類:
• Thread:創建線程
• Lock/RLock:互斥鎖
Thread
Thread構造方法
• 構造方法: Thread(group=None, target=None, name=None, args=(), kwargs={})
• group: 線程組,目前還沒有實現,庫引用中提示必須是None;
• target: 要執行的方法;
• name: 線程名;
• args/kwargs: 要傳入方法的參數。
Thread實例方法
• t.name:獲取或設置線程的名稱
• t.getName()/setName(name): 獲取/設置線程名。
• t.is_alive()、t.isAlive():判斷線程是否爲激活狀態。返回線程是否在運行。正在運行指啓動後、終
止前。
• t.ident :獲取線程的標識符。線程標識符是一個非零整數,只有在調用了start()方法之後該屬性才
有效,否則它只返回None
• t.run() :線程被cpu調度後自動執行線程對象的run方法
• t.start(): 線程準備就緒,等待CPU調度,start會自動調用t.run()
Thread實例方法
• t.join([timeout]): 阻塞當前上下文環境的線程,直到調用此方法的線程終止或到達指定的timeout
(可選參數)。
• t.setDaemon(bool): 設置是後臺線程(默認前臺線程(False))。(在start之前設置)
• 如果是後臺線程,主線程執行過程中,後臺線程也在進行,主線程執行完畢後,後臺線程不論
成功與否,主線程和後臺線程均停止
• 如果是前臺線程,主線程執行過程中,前臺線程也在進行,主線程執行完畢後,等待前臺線程
也執行完成後,程序停止
• t.isDaemon:判斷是否爲後臺線程
小案例
多線程抓取網頁數據
• 比較與傳統方式上,時間消耗
join的使用
對比使用join前後,時間消耗
setDaemon的使用
• 觀察使用setDaemon前後的差異
如何獲取線程的結果
• 定義一個全局變量、寫文件、寫Web接口… • 自定義線程類
• 自定義線程獲取結果
資源總是有限的,程序運行如果對同一個對象進行操作,則有可能造成
一些異常情況,如:
• 數據前後讀取不一致
• 資源的爭用甚至導致死鎖
Lock
在多線程中使用lock可以讓多個線程在共享資源的時候遵循一定的規則。
常見鎖類型
• Lock()/RLock:普通鎖(互斥鎖)
解決資源爭用,數據讀取不一致等
• Semaphore :信號量
最多允許同時N個線程執行內容
• Event: 事件鎖
根據狀態位,決定是否通過事件
• Condition: 條件鎖
Lock()/RLock:普通鎖(互斥鎖)
• 解決資源爭用,數據讀取不一致等
構造方法:
Lock()互斥鎖
實例方法:
• acquire([timeout]): 嘗試獲得鎖定。使線程進入同步阻塞狀態。
• release(): 釋放鎖。使用前線程必須已獲得鎖定,否則將拋出異常。
import random
import time
from threading import *
num = 1
lock = Lock()
def task():
name = current_thread().name
print(f"{name}start")
time.sleep(1)
print(f"{name}需要鎖了")
global num
#加鎖
lock.acquire()
num += 1
print(f"{name}curent num is => {num}")
time.sleep(random.random())
print(f"{name}curent num is => {num}")
lock.release()
print(f"{name}線程結束")
def main():
print("start")
for i in range(10):
t = Thread(target=task,args=())
t.start()
print("end")
if __name__ == "__main__":
"""產生了不一致讀的情況"""
main()
Semaphore :信號量
最多允許同時N個線程執行內容
構造方法:
• Semaphore (N)
實例方法:
• acquire([timeout]): 嘗試獲得鎖定。使線程進入同步阻塞狀態。
• release(): 釋放鎖。使用前線程必須已獲得鎖定,否則將拋出異常。
import random
import time
from threading import *
num = 0
lock = BoundedSemaphore(2)
def task():
name = current_thread().name
print(f"{name}start")
time.sleep(1)
print(f"{name}需要鎖了")
global num
#加鎖
with lock:
num += 1
print(f"{name}curent num is => {num}")
time.sleep(random.random())
print(f"{name}curent num is => {num}")
print(f"{name}線程結束")
def main():
print("start")
for i in range(10):
t = Thread(target=task,args=())
t.start()
print("end")
if __name__ == "__main__":
main()
Event: 事件鎖
• 事件機制:全局定義了一個“Flag”
• 如果“Flag”的值爲False,那麼當程序執行wait方法時就會阻塞
• 如果“Flag”值爲True,那麼wait方法時便不再阻塞。
• 這種鎖,類似交通紅綠燈(默認是紅燈),它屬於在紅燈的時候一次性阻擋所有線程,在綠燈的時
候,一次性放行所有的排隊中的線程。
• Event是線程間通信最間的機制之一:
• 一個線程發送一個event信號,其他的線程則等待這個信號。
用於主線程控制其他線程的執行。
實例方法:
• e.wait([timeout]) : 堵塞線程,直到Event對象內部標識位被設爲True或超時(如果提供了參數
timeout)
• e.set() :將標識位設爲Ture
• e.clear() : 將標識伴設爲False
• e.isSet() :判斷標識位是否爲Ture
from threading import *
event = Event()
def task():
name = current_thread().name
print(f"{name}start")
#檢查當前event的flag是不是true,如果是True,繼續往後執行
event.wait()
print(f"{name}end")
def main():
for i in range(10):
t = Thread(target=task,args=())
t.start()
event.clear()
while True:
flag = input("是否放行(1放行,0不放行,其他退出):")
if flag == "1":
event.set()
break
elif flag == "0":
event.clear()
else:
break
if __name__ == "__main__":
main()
Condition: 條件鎖
該機制會使得線程等待,只有滿足某條件時,才釋放n個線程。
實例方法:
• wait_for(func): 等待函數返回結果,如果結果爲True-放行一個線程
• wait、lock.notify(N): 一次性放行N個wait
• acquire、release: 以上的wait和wait_for需要在鎖中間使用
from threading import *
lock = Condition()
def task():
name = current_thread().name
print(f"{name}start")
with lock:
lock.wait()
print(f"{name}end")
def main():
for i in range(10):
t = Thread(target=task,args=())
t.start()
while True:
n = input("請輸入數字,表示放行幾個線程,輸出其他則退出")
if n.isdigit():
n = int(n)
with lock:
lock.notify(n)
else:
break
if __name__ == "__main__":
main()
from threading import *
lock = Condition()
def condition():
flag = input("請輸入0或1表示放行或者不放行")
if flag == "0":
return False
else:
return True
def task():
name = current_thread().name
print(f"{name}=>start")
with lock:
lock.wait_for(condition)
print(f"{name}=>end")
def main():
for i in range(10):
t = Thread(target=task,args=())
t.start()
if __name__ == "__main__":
main()
GIL
Python GIL與多線程
• GIL全稱Global Interpreter Lock(全局解釋器鎖)
• GIL和Python語言沒有任何關係,只是因爲歷史原因導致在官方推薦的解釋器Cpython中遺留的問
題(Jpython無此類問題)
• 每個線程在執行的過程中都需要先獲取GIL,保證同一時刻只有一個線程可以執行代碼
Python中多線程
• GIL最基本的行爲只有下面兩個:
- 當前執行的線程持有GIL
- 當線程遇到io阻塞時,會釋放GIL
• 由於GIL鎖的限制,所以多線程不適合計算型任務,而更適合IO型任務
• 計算密集型任務:用CPU、計算
=> 多進程
• IO密集型任務:網絡IO(抓取網頁數據)、磁盤操作(讀寫文件)、鍵盤輸入… => 多線程+多進程
進程與多進程
進程
• 概念:進程(Process)是計算機中的程序關於某數據
集合上的一次運行活動,是系統進行資源分配的基本單
位。
• 多個進程同時執行時,每個進程的執行都需要由操作系
統按一定的算法(RR調度、優先數調度算法等)分配
內存空間。
• 組成:進程控制塊PCB、數據段、正文段
• 基本狀態:就緒狀態、運行狀態和阻塞狀態
進程
• 創建:用戶創建出來的所有進程都是由操作系統負責,新進程的創建都是由一個已經存在的進程執
行了一個用於創建進程的系統調用而創建的
Linux中pid爲0的進程,是所有進程的主進程
• 如何創建子進程?
在python中,每一個運行的程序都有一個主進程,可以利用模塊中封裝的方法來創建子進程
(os.fork =>linux、multiprocessing)
os.fork創建子進程
os.fork中就用來創建子進程的方法
注意:這個os.fork()方法只有在unix系統中才會有,在window下沒有。
• 使用fork創建子進程後,操作系統會將當前的進程複製
一份
• 原來的進程稱爲父進程,新創建的進程稱爲子進程
• 兩個進程會各自互不干擾
的執行下面的程序
• 父進程與子進程的執行順序與系統調度有關
• 在子進程內,這個方法會返回0;在父進程內,這個方法會返回子進程的編號PID
• os.fork的返回值:
• 返回值爲大於0時,此進程爲父進程,且返回的數字爲子進程的PID;
• 當返回值爲0時,此進程爲子進程。
• 如果返回值爲負數則表明創建子進程失敗。
• 父進程結束時,子進程並不會隨父進程立刻結束。同樣,父進程不會等待子進程執行完。
os.getpid():獲取進程的進程號。
os.getppid():獲取父進程的進程號
import os
import time
pid = os.fork()
print(f"{pid} is pid")
if pid == 0:
print("這是一個子進程")
print("子進程的pid:",os.getpid())
print("父進程的pud:",os.getppid())
elif pid > 0:
print("這是一個父進程")
print("子進程的pid:", os.getpid())
print("父進程的pud:", os.getppid())
else:
print("創建失敗")
time.sleep(1)
#輸出結果
[root@lirixiang ~]# vim t.py
YouCompleteMe unavailable: requires Vim 7.4.1578+.
Press ENTER or type command to continue
[root@lirixiang ~]# python3 t.py
11831 is pid#子進程的pid
這是一個父進程
子進程的pid: 11830#父進程的pid
父進程的pud: 11799
0 is pid
這是一個子進程
子進程的pid: 11831
父進程的pud: 11830
小 案 例
在Linux上使用os.fork創建子進程
觀察:ps可查看到兩個進程
multiprocessing.Process
由於windows沒有fork調用,python提供了multiprocessing支持跨平臺版本。
創建管理進程模塊:
• Process
(用於創建進程模塊)
• Pool
(用於創建管理進程池)
• Queue
(用於進程通信,資源共享)
• Value,Array(用於進程通信,資源共享)
• Pipe(用於管道通信)
• Manager(用於資源共享)
Process 類
構造方法:Process([group [, target [, name [, args [, kwargs]]]]])
• group: 線程組,目前還沒有實現,庫引用中提示必須是None;
• target: 要執行的方法;
• name: 進程名;
• args/kwargs: 要傳入方法的參數。
實例方法:
• p.start():啓動進程,並調用該子進程中的p.run()
• p.run(): strat()調用run方法,如果實例進程時未制定傳入target,這star執行t默認run()
• p.terminate(): 不管任務是否完成,立即停止工作進程
• p.is_alive(): 如果p仍然運行,返回True
• p.join([timeout]): 阻塞當前上下文環境的進程,直到調用此方法的進程終止或到達指定的timeout
import time
from multiprocessing import *
li = []
def task(i):
process_name = current_process().name
print("start",process_name)
li.append(i)
print(li)
time.sleep(10)
print("end",process_name)
if __name__ == "__main__":
result = []
for i in range(10):
p = Process(target=task,args=(i,))
p.start()
#希望將各個子進程加起來,最後輸出
print("end")
小案例
使用multiprocessing.Process創建進程
使用multiprocessing.Process自定線程類創建進程類
進程間數據共享
不同進程間內存是不共享的,multiprocessing中提供以下方式實現進程間的數據交換
• Queue(用於進程通信,資源共享)
• Value,Array(用於進程通信,資源共享)
• Pipe(用於管道通信)
• Manager(用於資源共享)
使用multiprocessing.Array共享數據
• 創建Array時,需要指定數據類型
• 如:arr = Array(‘i’
, [11, 22, 33, 44])
•
'i’表示數據類型:“d”表示一個雙精度的浮點數,
“i”表示一個有符號的整數
• 這些共享對象將被線程安全的處理
,類型對應表
“c”: ctypes.c_char “u”: ctypes.c_wchar “b”: ctypes.c_byte “B”: ctypes.c_ubyte
“h”: ctypes.c_short “H”: ctypes.c_ushort “i”: ctypes.c_int “I”: ctypes.c_uint
“l”: ctypes.c_long,
“L”: ctypes.c_ulong “f”: ctypes.c_float “d”: ctypes.c_double
使用multiprocessing.Manager共享數 據
• 由Manager()返回的manager提供 list, dict, Namespace, Lock, RLock, Semaphore,
• BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array類型的支持。
• Manager比Array要好用一點,因爲它可以同時保存多種類型的數據格式
使用multiprocessing.Queue共享數據
• 消息隊列:multiprocessing.Queue
Queue是對進程安全的隊列,可以使用Queue實現對進程之間的數據傳輸;還有一個重要作用是作
爲緩存使用。
Queue(maxzize = 0) 創建一個隊列對象,maxsize 表示隊列中最多存放消息的數量。
• 實例方法:
• put(obj [, block=True[, timeout]]):調用隊列對象的put()方法將obj插入到隊列中
• get([block=True[, timeout]]):get方法可以將隊列中讀取並刪除一個元素
• full():判斷隊列是否爲滿
• empty():判斷隊列是否爲空
• qsize():獲取隊列中消息數量
Queue不能再Pool進程池中使用,使用Multiprocessing.Manager類可以適用Pool類
from multiprocessing import *
def task(q):
if not q.empty():
print("獲取一個數據",q.get())
if __name__ == "__main__":
q = Queue()
for i in range(10):
q.put(i)
q.put(i+10)
p = Process(target=task,args=(q,))
p.start()
print("end")
進程鎖
使用multiprocessing.Queue共享數據
• 不一致讀
• 爲了防止和多線程一樣的出現數據搶奪和髒數據的問題,同樣需要設置進程鎖。與threading類
似,在multiprocessing裏也有同名的鎖類RLock, Lock, Event, Condition, Semaphore,連用法
都是一樣樣的!
• 當創建進程時(非使用時),共享數據會被拿到子進程中,當進程中執行完畢後,再賦值給原值
multiprocessing.Pool進程池
多進程資源消耗
一般我們是通過動態創建子進程(或子線程)來實現併發服務器的,但是會存在這樣一些缺點:
- 動態
創建進程(或線程)比較耗費時間,消耗cpu資源
,這將導致較慢的服務器響應。 - 動態創建的子進程通常只用來爲一個客戶服務,這樣導致了系統上產生大量的細微進程(或
線程)。進程和線程間的切換
將消耗大量CPU時間。 - 動態創建的子進程是當前進程的
完整映像
,當前進程必須謹慎的管理其分配的文件描述符和
堆內存等系統資源,否則子進程可能複製這些資源,從而使系統的可用資源急劇下降,進而
影響服務器的性能。
所以呢,就引入了進程池的概念。
Pool 進程池
• 進程池的作用:有效的降低頻繁創建銷燬線程所帶來的額外開銷。
進程池的原理:
• 進程池都是採用預創建的技術
,在應用啓動之初便預先創建一定數目的進程。
• 應用在運行的過程中,需要時可以從這些進程所組成的進程池裏申請分配一個空閒
的進程,來執
行一定的任務,任務完成後,並不是將進程銷燬,而是將它返還給
進程池
,由線程池自行管理。
• 如果進程池中預先分配的線程已經全部分配完畢,但此時又有新的任務請求,則進程池會動態的 創建新的進程
去適應這個請求。
• 某些時段應用並不需要執行很多的任務,導致了進程池中的線程大多處於空閒的狀態,爲了節省
系統資源,進程池就需要動態的銷燬其中的一部分空閒進程
。
• 進程需要一個管理者
,按照一定的要求去動態的維護其中進程的數目。
Pool 主進程管理進程的機制:
• 最簡單、最常用的算法是隨機算法
和Round Robin
(輪流算法)
• 主進程和所有子進程通過一個共享的工作隊列來實現同步:子進程都睡眠在該工作隊列上,當有
新的任務到來時,主進程將任務添加到工作隊列中。這將喚醒正在等待任務的子進程,不過只有
一個子進程將獲得新任務的“接管權”
,它可以從工作隊列中取出任務並執行之,而其他子進程
將繼續睡眠在工作隊列上。
• 當選擇好子進程後,主線程程還需要使用某種通知機制來告訴目標子進程有新任務需要處理,並
傳遞必要的數據。我們可以把這些數據定義爲全局,那麼它們本身就是被所有進程共享的。對於
進程池而言,最簡單的方式是,在父進程和子進程之間預先建立好一條管道,然後通過管道來實
現所有的進程間通信。
Pool 進程池的應用場景
• 需要大量的進程來完成任務,且完成任務的時間比較短。
• 但對於長時間的任務,比如一個Telnet連接請求,進程池的優點就不明顯了。因爲Telnet會話時
間比線程的創建時間大多了。
Pool 類
構造方法
• Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])
• processes :使用的工作進程的數量,如果processes是None那麼使用 os.cpu_count()返回的
數量。
• initializer: 如果initializer是None,那麼每一個工作進程在開始的時候會調用
initializer(*initargs)。
• maxtasksperchild:工作進程退出之前可以完成的任務數,完成後用一個新的工作進程來替代
原進程,來讓閒置的資源被釋放。maxtasksperchild默認是None,意味着只要Pool存在工作進
程就會一直存活。
Pool 類
實例方法
• apply_async(func[, args[, kwds[, callback]]]) 它是非阻塞。
• apply(func[, args[, kwds]])是阻塞的。
• close() 關閉pool,使其不在接受新的任務。
• terminate() 關閉pool,結束工作進程,不在處理未完成的任務。
• join() 主進程阻塞,等待子進程的退出, join方法要在close或terminate之後使用。這樣是因爲被
終止的進程需要被父進程調用wait(join等價與wait),否則進程會成爲殭屍進程
注意:
① 使用Pool創建進程池對象,同時進程池中進程已經啓動
② 向進程池對象中添加事件,事件排隊執行
③ 如果主進程退出,則進程池中所有進程都退出
import time
from multiprocessing import *
def task():
print(current_process().name,"start")
time.sleep(0.5)
if __name__ == "__main__":
p = Pool(4)
for i in range(10):
p.apply_async(func=task)
#進程池不再接收新的任務
p.close()
# 等待子進程執行完畢以後,關閉進程池
p.join()
小案例
進程池的基本使用
完成10000個計算fun的任務
對比使用Poll和Process的差異
Python中多進程與多線程
• 多進程與多線程的選擇
- io密集型計算用多線程
- cpu密集型計算用多進程
多進程與多線程的生產者消費者模式
什麼是生產者消費者模式
什麼是生產者消費者模式
爲什麼要用生產者消費者模式解決問題?好處?
效率更高
增加項目的可擴展件
減少系統之間的耦合性
什麼場景下使用生產者消費者模式?
有明顯的兩個角色(創佳效據私處埋數據)
小案例
多線程的生產者消費者模式
多進程的生產者消費者模式
from multiprocessing import *
import os,time
def producer(q):
"""生產者,把數據放入消息隊列的人"""
for i in range(20):
time.sleep(1)
msg = f"{os.getpid()}生產數據"
q.put(msg)
print(msg)
def consumer(q):
"""消費者,從消息隊列取出數據的人"""
for i in range(10):
#當q.get沒有數據,阻塞狀態
time.sleep(2)
q.get()
msg = f"-----------{os.getpid()}消費了數據"
print(msg)
if __name__ == "__main__":
q =Queue()
#創建生產者
p1 = Process(target=producer,args=(q,))
p2 = Process(target=producer,args=(q,))
p1.start()
p2.start()
#創建消費者
c1 = Process(target=consumer,args=(q,))
c2 = Process(target=consumer,args=(q,))
c1.start()
c2.start()
#生產者消費者模式的好處,可擴展性=>以便生產和消費平衡狀態
#celery => 產生數據太大,導致redis中數據堆積
#讓系統可以發揮最大的性能,同時保證資源不浪費=>增加項目的可拓展性
有順序Queue
協程
概念
• 協程是一種用戶態
的輕量級線程,協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧
。
• 協程,又稱微線程,纖程,英文名Coroutine
。
• 協程的作用:
• 在執行函數A時,可以隨時中斷,去執行函數B,然後中斷繼續執行函數A(可以自由切換)。
• 但這一過程並不是函數調用(沒有調用語句)
協程與線程比較
• 線程有自己的上下文,切換受系統
控制;而協程有自己的上下文,但是其切換由用戶
控制,由當前協
程切換到其他協程由當前協程來控制。
• 無需原子操作鎖定及同步的開銷,所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦
開始,就一直運行到結束,中間不會有任何切換到另一個線程的動作。
線程=>切換出os控制
協程=>切換由user控制
留
原子操作(不會被線稈調度札制撲斷),協程的執行過程中,不會切換到另外一一個線稈動作
• 協程避免了無意義的調度,由此可以提高性能,但也因此,程序員必須自己承擔調度的責任,同時,協程也失去了標準線程使用多CPU的能力。
• 與多線程比協程有極高的執行效率
,不需要多線程的鎖機制。
• 協程以後主要用在網絡爬蟲和網絡請求,開闢一個協程大概需要5k空間,開闢一個線程需要512k空間, 開闢一個進程佔用資源最多。
• 強調非阻塞異步併發的一般都是使用協程
協程與線程的比較
假設有一個操作系統,是單核的,系統上沒有其他的程序需要運行,有兩個線程 A 和 B 。
• A 和 B 在單獨運行時都需要 10 秒來完成自己的任務,而且任務都是運算操作
,A B 之間也沒有競爭和共享數據的問題。
• 現在 A B兩個線程並行
,操作系統會不停的在 A B 兩個線程之間切換,達到一種僞並行的效果,假
設切換的頻率是每秒一次,切換的成本是 0.1 秒(主要是棧切換),總共需要 20 + 19 * 0.1 = 21.9
秒。
• 如果使用協程的方式
,可以先運行協程 A ,A 結束的時候讓位給協程 B ,只發生一次切換,總時間
是 20 + 1 * 0.1 = 20.1 秒。
協程的缺點:
• 無法利用多核資源
python對協程的支持
• python2.x協程應用:
• yield
• gevent
• python3.x協程應用:
• asynico + yield from(python3.4)
• asynico + await(python3.5)
• gevent
• Python3.4以後引入了asyncio模塊,可以很好的支持協程
協程—yield
從句法上看,協程與生成器類似,都是定義體中包含 yield 關鍵字的函數, 在協程中yield通常出現在表達式的右邊,可以產出值,也可以不產出,如果 yield 關鍵字後面沒有表達式,那麼生成器產出 None
協程可能會從調用方接收數據,不過調用方把數據提供給協程使用的是 .send(datum) 方法,而不是next(…) 函數.
asyncio協程
• asyncio是一個使用async / await語法編寫併發代碼的庫。
• asyncio用作多個Python異步框架的基礎,這些框架提供高性能的網絡和Web服務器,數據庫
連接庫,分佈式任務隊列等。
• asyncio通常非常適合IO綁定和高級結構化網絡代碼。
官方幫助:https://docs.python.org/3/library/asyncio.html?highlight=async#module-asyncio
asyncio提供了一組高級 API:
• 同時運行Python協同程序並完全控制它們的執行
;
• 執行網絡IO和IPC ;
• 控制子過程 ;
• 通過隊列分配任務;
• 同步併發代碼;
此外,還有一些用於庫和框架開發人員的低級 API :
• 創建和管理事件循環,提供異步的 hronous API networking,運行subprocesses,處理等;OS signals
• 使用傳輸實現有效的協議 ;
• 使用 async / await語法橋接基於回調的庫和代碼。
asyncio的幾個概念
• event_loop 事件循環:程序開啓一個無限的循環,程序員會把一些函數(協程)註冊到事件循環
上。當滿足事件發生的時候,調用相應的協程函數。
• coroutine 協程:協程對象,指一個使用async關鍵字定義的函數,它的調用不會立即執行函數,而是會返回一個協程對象。協程對象需要註冊到事件循環,由事件循環調用。
• future 對象: 代表將來執行或沒有執行的任務的結果。它和task上沒有本質的區別
• task 任務:一個協程對象就是一個原生可以掛起的函數,任務則是對協程進一步封裝,其中包含任務的各種狀態。Task 對象是 Future 的子類,它將 coroutine 和 Future 聯繫在一起,將
coroutine 封裝成一個 Future 對象。
• async/await 關鍵字:python3.5 用於定義協程的關鍵字,async定義一個協程,await用於掛起阻塞的異步調用接口。其作用在一定程度上類似於yield。
小 案 例
協程的基本使用
import asyncio
async def nested():
return "nested ok"
async def say_after(delay,what):
#休息delay秒
#調用協程函數
await asyncio.sleep(delay)
print(what)
return "WoW!"
#定義一個協程函數
async def main():
#調用協程函數
print(await say_after(1,"sanchuang!"))
#調用協程函數
task =asyncio.create_task(say_after(2,"world!"))
print(await task)
print(await nested())
asyncio.run(main())
協程asyncio串行執行多任務
協程asyncio並行執行多任務
import asyncio
async def do_some_work(x):
#sleep時長,協程執行效果,x sleep時間
await asyncio.sleep(x)
print(f"finished{x} second")
return x
#創建協程對象
task1 = do_some_work(1)
task2 = do_some_work(2)
task3 = do_some_work(4)
#print(task1,type(task1))
#將協程對象定義任務列表
tasks = [
asyncio.ensure_future(task1),
asyncio.ensure_future(task2),
asyncio.ensure_future(task3),
]
#調用協程函數
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
#獲取結果
for task in tasks:
print(task.result())
線程協程運行效率對比
"""
計算型任務=>多線程不如單線程,多線程不如協程 i
爲什麼=>本身是CPU型任務,那麼多線程額外消耗CPU切換和GIL鎖的時間....
I0型任務=>
1.任務內容=> sLeep(1)秒
2.創建10000個任務
3.記錄消耗時間
"""
#協程
import asyncio
async def task():
await asyncio.sleep(1)
#創建1w個協程對象
coroutines = [task() for i in range(10000)]
#轉換任務
tasks = [asyncio.ensure_future(coro) for coro in coroutines]
#執行任務
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
#結果
real 0m5.883s
user 0m2. 949s
sys 0m2.395s
#線程
import time
import threading
def task():
time.sleep(1)
for i in range(20000):
t = threading.Thread(target=task)
t.start()
#結果
rea1 0m1.844s
user 0m0.9743
sys OmO.048s
協程vs線程
1.協程=>用戶控制任務調度,原子性(不會被操作系統切換出去)
線程=> os操控任務調度
2.資源消耗=>迸程>線程>協程
3. 協程的優勢?多進程+協程
python對協程的支持3.5
阻塞=>任務來了->需要其他資源->如果資源沒有到位-> - 直等待
同步阻塞=>一 次執行- 一個任務,依次執行
異步非阻塞=> 多個任務可以同時進行,如果任務 需要資源了,切換到其他任務 去執行
協程隊列(https://docs.python.org/3/library/asyncio-queue.html#asyncio-queues)
協程鎖(https://docs.python.org/3/library/asyncio-sync.html#asyncio-sync)
課 後 作 業
批量掃描指定網段內的存活IP
• 網段:192.168.0.0/24 (192.168.0.1~192.168.0.255)
• 考慮如何才能更快速地掃描出結果
• 前置模塊:IPy, ping命令的使用
什麼是GIL
什麼是進程、線程,兩者關係如何
現在有t1、t2、t3三個線程,你怎樣保證t2在t1執行完後執行,t3在t2執行完後執行
函數A在不停產生數據(數據來源於文件),函數B用處理A產生的數據,B處理每次處理數據都需要2s。
●請使用分別使用生產者消費者模式,1個生產者,4個消費者
●函數A,實現產生數據(從文件中取出數據)
●函數B,實現處理數據(將數據轉化爲大寫後, sleep 2s,寫入文件)
●需要保證生成的目標文件,數據順序與原來保持一致