執行緒的基礎知識
阿新 • • 發佈:2022-04-21
概要
-
訊息佇列
-
IPC(inter-producer-consumer)機制(程序間通訊)
-
生產者消費模型
-
執行緒理論(重要)
-
開設執行緒的兩種方式
-
執行緒實現TCP(Transmission Control Protocol)服務端併發
-
執行緒join方法
-
執行緒間資料共享
-
守護執行緒
-
GIL(Global-interpreter-lock)全域性直譯器鎖
內容
1.訊息佇列
由於目前的知識儲備還不夠直接學習訊息佇列 所以先學習內建中的佇列
佇列:先進先出(使用頻率很高 因為現實生活中大部分都是使用的是佇列) 隊棧:先進後出(只在一些特定場景下會使用到) # 以後我們會直接使用別人封裝好的訊息佇列 實現各種資料傳輸 from multiprocessing import Queue q = Queue(5) # 自定義佇列的長度 q.put(111) q.put(222) q.put(333) print(q.full()) # False 判斷佇列是否滿了 q.put(444) q.put(555) print(q.full()) # True # q.put(666) # 超出最大長度 原地阻塞等待佇列中出現空位 print(q.get()) print(q.get()) print(q.empty()) # False 判斷佇列是否空了 print(q.get()) print(q.get()) print(q.get()) print(q.empty()) # True # print(q.get_nowait()) # 如果佇列中沒有值 計算機會報錯 """ full() 判斷佇列是否滿了》》》轉為布林值 empty() 判斷佇列是否空了>>>>轉為布林值 get_nowait() 如果佇列中沒有值 直接保錯 上述方法能否在併發的場景下精準使用? 答案是不能 因為我們可以往極限方向去想 就是在一個程序中 1.我在取的時候 已經取完了然後使用empty() 然後在那瞬間put這邊又要來一個值 這裡通empty()出來的結果還算是正確的結果嗎 2.我在放在時候 已經全部放完後使用full(),然後在那瞬間get()這邊又取走了一個值 這裡通過full()出來的結果還算是正確的結果嗎 綜上:不能用 之所以還介紹佇列的目的是因為它還支援程序間資料通訊 我們需要學習程序間資料通訊 """
2.IPC(inter-producer-consumer)機制(程序間通訊)
1.主程序與子程序資料互動 2.兩個子程序資料互動 本質:不同記憶體空間中的程序資料互動 from multiprocessing import Process,Queue def producer(q): q.put('子程序producer從佇列中新增值') def consumer(q): print('子程序consumer從佇列中取值>>>>:',q.get()) if __name__ == '__main__': q = Queue() p = Process(target = producer,args =(q,)) p1 = Process(target = consumer,args=(q,)) p.start() p1.start() # q.put(123) # 主程序往佇列中存放資料123 print('主程序') 執行之後產生的結果是: 主程序 子程序consumer從佇列中取值>>>: 子程序producer往佇列中新增值
3.生產者消費者模型
# 生產者 負責生產/製作資料 # 消費者 負責消費/處理資料 比如在爬蟲領域中 會先通過程式碼爬取網頁資料 (爬取網頁的程式碼就可以稱之為是生產者) 之後針對網頁資料做篩選處理(處理網頁的程式碼就可以稱之為消費者) 如果使用程序來掩飾 除了有至少兩個程序之外 還需要一個媒介(訊息佇列) 以後遇到該模型需要考慮的問題其實就是供需平衡的問題 生產力與消費力要均衡 from multiprocessing import Process, Queue, JoinableQueue JoinableQueue import time import random def producer(name,food,q): for i in range(5): data = f'{name}生產了{food}{i}' print(data) time.sleep(random.randint(1,3)) # 模擬生產過程 q.put(data) def consumer(name,q): while True: food = q.get() time.sleep(random.random()) print(f'{name}吃了{food}') q.task_done() # 每次去完成資料必須給佇列一個反饋 if __name__ == '__main__': q = JoinableQueue() p1 = Process(target=producer, args=('大廚jason', '沙縣小吃', q)) p2 = Process(target=producer, args=('大廚kevin', '韭菜炒蛋', q)) c1 = Process(target=consumer,args=('tony', q)) c2 = Process(target=consumer,args=('jerry', q)) c1.daemon = True c2.daemon = True p1.start() p2.start() c1.start() c2.start() # 生產者生產完所有資料之後 往佇列中新增結束的訊號 p1.join() p2.join() # q.put(None) # 結束訊號的個數要跟消費者個數要一致才可以 # q.put(None) # 佇列中其實已經自己加了鎖 所有多程序取值也不會衝突 並且取走了就沒了 q.join() # 等待佇列中資料全部被取出(一定要讓生產這全部結束才能判斷正確) """ 執行完上述的join方法表示消費者也已經消費完資料了"""
4.執行緒理論
# 什麼是執行緒
程序:相當於一個資源單位
執行緒:程序中的執行單位
程序相當於車間(一個個空間),執行緒相當於車間裡面的流水線(真正幹活的)
"""一個程序中至少有一個執行緒
程序僅僅是在記憶體中開闢一塊空間(提供執行緒工作所需的資源)
執行緒真正被CPU執行 執行緒需要的資源跟所在程序的去拿就可以"""
# 為什麼要有執行緒
開設執行緒的消耗遠遠小於程序
開程序過程:1.申請記憶體空間 2.拷貝程式碼
開執行緒:一個程序內可以開設多個執行緒 無需申請記憶體空間 拷貝程式碼
一個程序內的多個執行緒資料是共享的 都是在同一個執行緒內 大家的資源都是共享的
"""開發一個文字編輯器
獲取使用者輸入並實時展示到螢幕上
並實時儲存到硬碟中
多種功能應該開設多執行緒而不是多程序"""
5.開設執行緒的兩種方式
'程序與執行緒的程式碼實操幾乎是一樣的 主要是因為執行緒與程序的理論是同一個人寫的 所以兩者的用法幾乎一樣'
from threading import Thread
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
# 建立執行緒無需在__main__下面編寫 但是為了統一 還是習慣在子程式碼中寫
t = Thread(target=task,args=('jason',))
t.start() # 建立執行緒的開銷極小 幾乎是一瞬間就可以建立
print('主執行緒')
class MyThread(Thread):
def __init__(self,username):
super().__init__()
self.username = username
def run(self):
print(f'{self.username} jason is running')
time.sleep(3)
print(f'{self.username} is over')
t = MyThread('jasonNB')
t.start()
print('主執行緒')
6.執行緒實現TCP服務端的併發
需要比對開設程序與執行緒的本質區別
import socket
from threading import Thread
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen()
def talk(sock): # 封裝成一個函式為了實現多個服務實現併發的效果
while True:
data = sock.recv(1024)
print(data.dacode('utf8'))
sock.send(data.upper())
while True:
sock,addr = server.accept()
# 每類一個客戶端就建立一個執行緒做資料互動
t = Thread(target=talk,args=(sock,))
t.start()
7.線下join方法
from threading import Thrad
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
t = Thread(target=task,args=('jason',))
t.start()
t.join() # 是指子執行緒程式碼執行結束完畢後再往下之行
print('主執行緒')
"主執行緒為什麼要等著子執行緒結束後才會執行整個程序
因為主執行緒結束也就標誌著整個程序的結束 為要確保子執行緒執行過程中所需要的各項資源 所以主執行緒不能先結束
"
8.同一個程序內的多個執行緒資料共享
from threading import Thread
money = 10000
def task():
global money
money = 1
t = Thread(target=task)
t.start()
t.join()
print(money) # 1
9.執行緒物件屬性與方法
1.驗證一個程序下的多個執行緒是否真的處於一個程序
驗證確實如此
2.統計程序下活躍的執行緒數
active_count() # 注意主執行緒也算!!
3.獲取執行緒的名字
1.current_thread().name
MainThread 主執行緒
Thread-1、Thread-2 子執行緒
2.self.name
10.守護執行緒
from threading import Thread
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
t1 = Thread(target=task, args=('jason',))
t2 = Thread(target=task, args=('kevin',))
t1.daemon = True
t1.start()
t2.start()
print('主執行緒')
11.GIL全域性直譯器鎖
純理論 面試題目
# 官方文件
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
1.回顧
python直譯器的類別有很多(根據直譯器是由什麼編寫的)
C-python、J-python、P-python(pyhton)
垃圾回收機制
應用計數、標記清除、分代回收
"""
GIL只存在於Cpython直譯器中 不是python的特徵
GIL是一把互斥鎖用於阻止同一個程序下的多個執行緒同時執行
原因是因為Cpython直譯器中的垃圾回收機制不是執行緒安全的
反向驗證GIL的存在 如果不存在會產生垃圾回收機制與正常執行緒之間資料錯亂
GIL是加在Cpython直譯器上面的互斥鎖
同一個程序下的多個執行緒要想執行必須先前搶GIL鎖 所以同一個程序下多個執行緒肯定不能同時執行 即無法利用多核優勢
強調:同一個程序下的多個執行緒不能同時執行即不能利用多核優勢
很多不懂python的程式設計師會噴python是垃圾 速度太慢 有多核都不能用
反懟:雖然用一個程序下的多個執行緒不能利用多核優勢 但是還可以開設多程序
再次強調:python的多執行緒就是垃圾
反懟:要結合實際情況而定
如果多個任務都是IO密集型 那麼多執行緒更有優勢(消耗的資源更少) 多道技術:切換+儲存狀態
如果多個任務都是計算密集型 那麼多執行緒確實沒有優勢 但是可以用多程序來彌補
cpu 越多越好
以後用python就可以多程序下面開設多執行緒從而達到效率最大化
"""
ps:1.所有的解釋型語言都無法做到同一個程序下多個執行緒利用多核優勢
2.GIL在實際程式設計中其實不用考慮