目錄
線程編程(Thread)
線程基本概念
- 什麼是線程
【1】線程被稱爲輕量級的進程
【2】線程也可以使用計算機多核資源,是多任務編程方式
【3】線程是系統分配內核的最小單元(有可能多個線程佔用多個內核)
【4】線程可以理解爲進程的分支任務
- 線程特徵
【1】一個進程中可以包含多個線程
【2】線程也是一個運行行爲,消耗計算機資源
【3】一個進程中的所有線程共享這個進程的資源
【4】多個線程之間的運行互不影響各自運行
【5】線程的創建和銷燬消耗資源遠小於進程
【6】各個線程也有自己的ID等特徵
threading模塊創建線程
【1】 創建線程對象
from threading import Thread
t = Thread()
功能:創建線程對象
參數:target 綁定線程函數
args 元組 給線程函數位置傳參
kwargs 字典 給線程函數鍵值傳參
【2】 啓動線程
t.start()
【3】 回收線程
t.join([timeout])
單線程(無參數):
from threading import Thread
from time import sleep
import os
a=1
def music():
global a
print("a=",a)# 1
a = 10000
for i in range(5):
sleep(2)
print(os.getpid(),"播放心如止水")
# 創建線程對象
t = Thread(target=music)
t.start()
# 主線程任務
for i in range(3):
sleep(3)
print(os.getpid(),"播放跳舞吧")
t.join() # 回收進程
# 同一進程,分支線程將變量值更改,主線程變量值也隨之更改(區分多進程,兩個進程間互不影響)
print("Main thread a:",a)#10000
多線程(有參數):
from threading import Thread
from time import sleep
# 含有參數的線程函數
def fun(sec, name):
print("線程函數傳參")
sleep(sec)
print("%s 線程執行完畢" % name)
# 創建多個線程
jobs = []
for i in range(5):
t = Thread(target=fun, args=(2,),
kwargs={'name': 'T%d' % i})
jobs.append(t)
t.start()
for i in jobs:
i.join()
線程對象屬性
t.name 線程名稱
t.setName() 設置線程名稱
t.getName() 獲取線程名稱
t.is_alive() 查看線程是否在生命週期
t.daemon 設置主線程和分支線程的退出關係
t.setDaemon() 設置daemon屬性值
t.isDaemon() 查看daemon屬性值
*daemon爲True時主線程退出分支線程也退出。要在start前設置,通常不和join一起使用
from threading import Thread
from time import sleep
def fun():
sleep(3)
print("線程屬性測試")
t = Thread(target=fun, name="Tarena")
# 主線程退出分支線程也退出
t.setDaemon(True)
t.start()
t.setName("Tedu")
print("Name:", t.getName()) # 線程名稱(默認Thread-1)
print("Alive:", t.is_alive()) # 線程生命週期
print("is Daemon", t.isDaemon())
自定義線程類
- 創建步驟
【1】繼承Thread類
【2】重寫__init__方法添加自己的屬性,使用super加載父類屬性
【3】重寫run方法 - 使用方法
【1】 實例化對象
【2】 調用start自動執行run方法
【3】 調用join回收線程
重點代碼:
from threading import Thread
class ThreadClass(Thread):
def __init__(self, attr):
self.attr = attr
super().__init__() # 調用父類的init方法
def f1(self):
print("步驟1", self.attr)
def f2(self):
print("步驟2")
# 父類方法重寫
def run(self):
self.f1()
self.f2()
t = ThreadClass('XXXXX')
t.start() # 自動運行run方法
t.join()
小練習:
from threading import Thread
from time import sleep, ctime
class MyThread(Thread):
# pass
# 解答如下:
def __init__(self, target=None, args=(),
kwargs={}, name="AAA"):
super().__init__()
self.target = target
self.args = args
self.kwargs = kwargs
self.name = name
def run(self):
self.target(*self.args, **self.kwargs)
# **********************************
# 通過完成上面的MyThread類讓整個程序可以正常執行
# 當調用start時player作爲一個線程功能函數運行
# 注意:函數名稱和參數並不確定,player只是測試函數
# **********************************
def player(sec, song):
for i in range(2):
print("Playing %s:%s" % (song, ctime()))
sleep(sec)
t = MyThread(target=player, args=(3,),
kwargs={'song': '涼涼'}, name='happy')
t.start()
t.join()
同步互斥
線程間通信方法
- 通信方法
線程間使用全局變量進行通信。 - 共享資源爭奪
- 共享資源:多個進程或者線程都可以操作的資源稱爲共享資源。對共享資源的操作代碼段稱爲臨界區。
- 影響 : 對共享資源的無序操作可能會帶來數據的混亂,或者操作錯誤。此時往往需要同步互斥機制協調操作順序。
- 同步互斥機制
同步 : 同步是一種協作關係,爲完成操作,多進程或者線程間形成一種協調,按照必要的步驟有序執行操作。
互斥 : 互斥是一種制約關係,當一個進程或者線程佔有資源時會進行加鎖處理,此時其他進程線程就無法操作該資源,直到解鎖後才能操作。
線程同步互斥方法
線程Event
from threading import Event
e = Event() 創建線程event對象
e.wait([timeout]) 阻塞等待e被set
e.set() 設置e,使wait結束阻塞(沒有這句話e.wait()一直堵塞)
e.clear() 使e回到未被設置狀態
e.is_set() 查看當前e是否被設置
重點代碼:
from threading import Thread, Event
s = None # 全局變量用於通信
e = Event()
def yang():
print("楊子榮前來拜山頭")
global s
s = "天王蓋地虎"
e.set() # 共享資源操作完畢
t = Thread(target=yang)
t.start()
print("說對口令就是自己人")
e.wait() # 堵塞等待
if s == "天王蓋地虎":
print("寶塔鎮河妖")
print("確認過眼神,你是對的人")
else:
print("打死他。。。")
t.join()
線程鎖 Lock
from threading import Lock
lock = Lock() 創建鎖對象
lock.acquire() 上鎖 如果lock已經上鎖再調用會阻塞
lock.release() 解鎖
with lock: 上鎖
...
...
with代碼塊結束自動解鎖
重點代碼:
from threading import Thread, Lock
a = b = 0
lock = Lock()
def value():
while True:
lock.acquire() # 上鎖
if a != b:
print("a=%d,b=%d" % (a, b))
lock.release() # 解鎖
t = Thread(target=value)
t.start()
while True:
with lock:
a += 1
b += 1
t.join()
死鎖及其處理
- 定義
死鎖是指兩個或兩個以上的線程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖。
2. 死鎖產生條件
死鎖發生的必要條件:
- 互斥條件:指線程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。如果此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
- 請求和保持條件:指線程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求線程阻塞,但又對自己已獲得的其它資源保持不放。
- 不剝奪條件:指線程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放,通常CPU內存資源是可以被系統強行調配剝奪的。
- 環路等待條件:指在發生死鎖時,必然存在一個線程——資源的環形鏈,即進程集合{T0,T1,T2,···,Tn}中的T0正在等待一個T1佔用的資源;T1正在等待T2佔用的資源,……,Tn正在等待已被T0佔用的資源。
- 死鎖的產生原因:
簡單來說造成死鎖的原因可以概括成三句話:
- 當前線程擁有其他線程需要的資源
- 當前線程等待其他線程已擁有的資源
- 都不放棄自己擁有的資源
- 如何避免死鎖
死鎖是我們非常不願意看到的一種現象,我們要儘可能避免死鎖的情況發生。通過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或者幾個,來預防發生死鎖。預防死鎖是一種較易實現的方法。但是由於所施加的限制條件往往太嚴格,可能會導致系統資源利用率。
- 使用定時鎖
- 使用重載鎖RLock(),用法同Lock。RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源可以被多次acquire。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。
使用定時鎖:
import time
import threading
# 交易類
class Account:
def __init__(self, _id, balance, lock):
self.id = _id # 用戶
self.balance = balance # 存款
self.lock = lock # 鎖
# 取錢
def withdraw(self, amount):
self.balance -= amount
# 存錢
def deposit(self, amount):
self.balance += amount
# 查看餘額
def get_balance(self):
return self.balance
# 創建賬戶
Tom = Account('Tom', 5000, threading.Lock())
Alex = Account('Alex', 8000, threading.Lock())
# 轉賬函數
def transfer(from_, to, amount):
if from_.lock.acquire(): # 鎖自己賬戶
from_.withdraw(amount) # 自己賬戶減少
time.sleep(0.5) # 產生死鎖(因爲from和to同時上鎖)
if to.lock.acquire(): # 對方賬戶上鎖
to.deposit(amount)
to.lock.release() # 對方賬戶解鎖
from_.lock.release() # 自己賬戶解鎖
print("%s 給 %s轉賬完成" % (from_.id, to.id))
t1 = threading.Thread(target=transfer,
args=(Tom, Alex, 2000))
t2 = threading.Thread(target=transfer,
args=(Alex, Tom, 2000))
t1.start()
time.sleep(2) # t2等待一段時間再上鎖,確保t1先執行完
t2.start()
t1.join()
t2.join()
print("Tom:", Tom.get_balance())
print("Alex:", Alex.get_balance())
使用重載鎖RLock():
from threading import Thread, RLock
import time
num = 0 # 共享資源
lock = RLock() # 在一個線程內可以對鎖進行重複加鎖
class MyThread(Thread):
def fun1(self):
global num
with lock:
num -= 1
def fun2(self):
global num
if lock.acquire():
num += 1
if num > 5:
self.fun1()
print("Num=", num)
lock.release()
def run(self):
while True:
time.sleep(2)
self.fun2()
t = MyThread()
t.start()
t.join()
Python線程GIL
簡單來說,就是利用python實現線程的效率低
- python線程的GIL問題(全局解釋器鎖)
什麼是GIL :由於python解釋器設計中加入瞭解釋器鎖,導致python解釋器同一時刻只能解釋執行一個線程,大大降低了線程的執行效率。
導致後果: 因爲遇到阻塞時線程會主動讓出解釋器,去解釋其他線程。所以python多線程在執行多阻塞高延遲IO時可以提升程序效率,其他情況並不能對效率有所提升。
GIL問題建議:
- 儘量使用進程完成無阻塞的併發行爲
- 不使用c作爲解釋器 (Java C#)
- 結論 : 在無阻塞狀態下,多線程程序和單線程程序執行效率幾乎差不多,甚至還不如單線程效率。但是多進程運行相同內容卻可以有明顯的效率提升。
作業:編寫程序完成效率測試
- 使用單線程執行計算密集函數十次記錄時間,執行IO密集函數十次記錄時間
- 使用10個線程分別執行計算密集函數1次記錄時間,執行IO密集函數1次記錄時間
- 使用10個進程分別執行計算密集函數1次記錄時間,執行IO密集函數1次記錄時間
計算密集型函數:
def count(x,y):
c = 0
while c < 7000000:
c += 1
x += 1
y += 1
IO密集型函數:
def io():
write()
read()
def write():
f = open('test','w')
for i in range(1500000):
f.write("hello world\n")
f.close()
def read():
f = open('test')
lines = f.readlines()
f.close()
- 【單線程】計算密集函數十次:8.780869245529175;IO密集型函數十次:4.685685396194458
- 【多線程】計算密集函數十次:14.451507091522217;IO密集型函數十次:6.815863847732544
- 【多進程】計算密集函數十次:4.909349203109741;IO密集型函數十次:2.378950357437134
進程線程的區別聯繫
區別聯繫
- 兩者都是多任務編程方式,都能使用計算機多核資源
- 進程的創建刪除消耗的計算機資源比線程多
- 進程空間獨立,數據互不干擾,有專門通信方法;線程使用全局變量通信
- 一個進程可以有多個分支線程,兩者有包含關係
- 多個線程共享進程資源,在共享資源操作時往往需要同步互斥處理
- 進程線程在系統中都有自己的特有屬性標誌,如ID,代碼段,命令集等。
使用場景
- 任務場景:如果是相對獨立的任務模塊,可能使用多進程,如果是多個分支共同形成一個整體任務可能用多線程。
- 項目結構:多種編程語言實現不同任務模塊,可能是多進程,或者前後端分離應該各自爲一個進程。
- 難易程度:通信難度,數據處理的複雜度來判斷用進程間通信還是同步互斥方法。
要求
- 對進程線程怎麼理解/說說進程線程的差異
- 進程間通信知道哪些,有什麼特點
- 什麼是同步互斥,你什麼情況下使用,怎麼用
- 給一個情形,說說用進程還是線程,爲什麼
- 問一些概念,殭屍進程的處理,GIL問題,進程狀態