線程
概述
通俗點講,在共享的空間里同時做不同的任務
進程和線程的區(qū)別
- 同一個進程中的所有線程共享同一內(nèi)存空間,但是2個進程之間內(nèi)存空間是獨立的。
- 同一個進程中的所有線程的數(shù)據(jù)是共享的,多個進程之間的數(shù)據(jù)是獨立的。
- 進程安全性高,線程安全性低
- 進程是資源分配的單元,線程是調(diào)度單元
python多線程編程
多線程修改全局變量
import time
from threading import Thread
"""多線程實現(xiàn)修改全局變量"""
# 定義一個全局變量
num = 0
print("num=", num)
def modify1():
global num
num += 1
print('修改一次num')
def modify2():
global num
num += 1
print('修改一次num')
def read():
print("讀出線程,num的值是", num)
# 創(chuàng)建一個線程
t1 = Thread(target=modify1)
t2 = Thread(target=modify2)
t3 = Thread(target=read)
# 啟動線程
t1.start()
t2.start()
t3.start()
# 睡眠一秒,防止線程未執(zhí)行完就輸出num
time.sleep(1)
print("主線程,num的值是", num)
類繼承
from threading import Thread
"""簡單多線程類繼承"""
# 創(chuàng)建一個自己的多線程繼承類
class MyThread(Thread):
def __init__(self, name):
# 調(diào)用父類的構(gòu)造方法
Thread.__init__(self)
# super(MyThread, self).__init__()
self.name = name
def run(self):
print("類繼承方式,我是", self.name)
# 實體化出來一個對象
myt1 = MyThread("江小白")
# 啟動線程,線程就去執(zhí)行類里的run方法
myt1.start()
線程的同步和互斥
臨界資源
多線程編程時主要考慮內(nèi)容:全局變量和代碼資源
全局變量訪問臨界區(qū)
from threading import Thread
global_variable = 0
# 線程1,寫數(shù)據(jù)
def write_data1():
global global_variable
for x in range(1000000):
global_variable += 1
print("線程1看到的global_variable:", global_variable)
# 線程2,寫數(shù)據(jù)
def write_data2():
global global_variable
for x in range(1000000):
global_variable += 1
print("線程2看到的global_variable:", global_variable)
# 主線程
def main():
t1 = Thread(target=write_data1)
t2 = Thread(target=write_data2)
t1.start()
t2.start()
t1.join()
t2.join()
print("主線程里看到的global_variable:", global_variable)
if __name__ == '__main__':
main()
代碼區(qū)訪問臨界資源
from threading import Thread
# 定義兩個全局變量
value1 = 0
value2 = 0
def writer_worker():
global value1
global value2
count = 0
while 1:
count += 1
# 使用兩句代碼改變那兩個全局變量
value1 = count
value2 = count
def check_worker():
while 1:
# 時間片輪算法導致value2 != value1,可以使用互斥鎖可解決這一問題
if value2 != value1:
print(f"value1:{value1} value2:{value2}")
if __name__ == '__main__':
t1 = Thread(target=writer_worker, name="寫線程")
t2 = Thread(target=check_worker, name="讀線程")
t1.start()
t2.start()
如何解決訪問臨界資源
- 使用python內(nèi)置的原子結(jié)構(gòu)設計臨界資源。
- list、dict的操作方法
- queue.Queue的隊列結(jié)構(gòu)
- 使用Lock(鎖)、Semaphore(信號量)。
- Lock對象(最常用的)
- Event對象(事件對象)
- Timer對象(定時器對象)
線程上的Lock對象
- 鎖機制是操作系統(tǒng)底層提供的一種互斥訪問的機制。
- 當一個任務獲取到未鎖定狀態(tài)的鎖時,任務繼續(xù)運行;
- 當一個任務獲取到以鎖定狀態(tài)的鎖時,任務將被放入等待隊列,直到這把鎖被釋放時被喚醒。
- threading提供了一個Lock類,來實現(xiàn)了這種鎖機制。
Lock代碼實例
import threading
value1 = 0
value2 = 0
def writer_worker():
global value1
global value2
count = 0
while True:
count += 1
# 上鎖
# v_lock.acquire()
with v_lock:
value1 = count
value2 = count
# 解鎖
# v_lock.release()
def check_worker():
while True:
# 上鎖
v_lock.acquire()
if value2 != value1:
print(f"+++++v1:{value1}++v2:{value2}+++")
# 解鎖
v_lock.release()
if __name__ == '__main__':
t1 = threading.Thread(target=writer_worker, name="寫線程")
t2 = threading.Thread(target=check_worker, name="讀線程")
# 買了一把鎖
v_lock = threading.Lock()
t1.start()
t2.start()
線程共享數(shù)據(jù)(臨界數(shù)據(jù))安全問題解決
from threading import Thread, Lock
num = 0
# 定義兩個函數(shù)
def add1():
global num
# 上鎖 理論:鎖住最小的單元
lock.acquire()
for i in range(1000000):
num += 1
# 第一步:num
# 第二步: temp = num+1
# 第三步:num = temp
# 解鎖
lock.release()
print(f"add1:{num}")
def add2():
global num
# 上鎖
lock.acquire()
for i in range(1000000):
num += 1
# 解鎖
lock.release()
print(f"add2:{num}")
# 買一把鎖
lock = Lock()
# 創(chuàng)建線程
t1 = Thread(target=add1)
t2 = Thread(target=add2)
# 啟動線程
t1.start()
t2.start()
# 等等線程結(jié)束
t1.join()
t2.join()
print(f"任務結(jié)束:{num}")
類似于事務:要么都成功,要么都失敗,就像同生共死那樣,保證數(shù)據(jù)的完整性
死鎖
產(chǎn)生原因:在線程間共享多個資源的時候,如果兩個線程分別占有一部分資源并且同時等待對方的資源,就會造成死鎖。
產(chǎn)生結(jié)果:盡管死鎖很少發(fā)生,但一旦發(fā)生就會造成應用的停止響應。下面看一個死鎖的例子
解決方法:設置超時時間
# coding=utf-8
import threading
import time
class MyThread1(threading.Thread):
def run(self):
if mutexA.acquire():
print(self.name + '----do1---up----')
time.sleep(1)
# 設置超時時間
if mutexB.acquire(blocking=True, timeout=2):
print(self.name + '----do1---down----')
mutexB.release()
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
if mutexB.acquire():
print(self.name + '----do2---up----')
time.sleep(1)
if mutexA.acquire():
print(self.name + '----do2---down----')
mutexA.release()
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
線程同步
利用鎖機制來解決線程同步,一旦同步,效率降低
import time
from threading import Thread, Lock
def f1():
while True:
if lock1.acquire():
print(1)
time.sleep(1)
# 解開2號鎖
lock2.release()
def f2():
while True:
if lock2.acquire():
print(2)
time.sleep(1)
# 解開3號鎖
lock3.release()
def f3():
while True:
if lock3.acquire():
print(3)
time.sleep(1)
# 解開1號
lock1.release()
# 鎖
lock1 = Lock()
# 鎖住2號
lock2 = Lock()
lock2.acquire()
# 鎖住3號
lock3 = Lock()
lock3.acquire()
# 創(chuàng)建三個線程
t1 = Thread(target=f1)
t2 = Thread(target=f2)
t3 = Thread(target=f3)
# 啟動線程
t1.start()
t2.start()
t3.start()
GIL
全局解釋鎖:為了保證線程的安全,在同一時刻,只有一個線程被CPU執(zhí)行。
解決方案:調(diào)用C
分析什么時候使用多進程和多線程
多進程:CPU密集型,需要CPU參與大量運算
多線程:IO密集型 ,執(zhí)行這個上任務的時候,CPU只需要很少,其它大部分時間都傳輸,文件傳輸
再加一條,如果你不知道你的代碼到底算CPU密集型還是IO密集型,教你個方法:
multiprocessing這個module有一個dummy的sub module,它是基于multithread實現(xiàn)了multiprocessing的API。
假設你使用的是multiprocessing的Pool,是使用多進程實現(xiàn)了concurrency
from multiprocessing import Pool
如果把這個代碼改成下面這樣,就變成多線程實現(xiàn)concurrency
from multiprocessing.dummy import Pool
兩種方式都跑一下,哪個速度快用哪個就行了。