1.多任務(wù)
在計(jì)算機(jī)中,操作系統(tǒng)可以同時(shí)運(yùn)行多個(gè)任務(wù),這就是多任務(wù)。那么如何解決多個(gè)任務(wù)同時(shí)運(yùn)行呢,那就需要用到多線程。多任務(wù)可以通過并發(fā)和并行來完成,那么什么是并發(fā)和并行呢?
? 1.并發(fā):指的是任務(wù)數(shù)多于cpu核數(shù),通過操作系統(tǒng)的各種任務(wù)調(diào)度算法,實(shí)現(xiàn)用多個(gè)任務(wù)“一起”執(zhí)行(實(shí)際上總有一些任務(wù)不在執(zhí)行,因?yàn)榍袚Q任務(wù)的速度相當(dāng)快,看上去一起執(zhí)行而已),在實(shí)際開發(fā)中,并發(fā)是最常用的。
? 2.并行:當(dāng)任務(wù)數(shù)小于或者等于cpu核數(shù)時(shí),每一個(gè)任務(wù)都有對應(yīng)的cpu來處理執(zhí)行,即任務(wù)真的是一起執(zhí)行的。
2.多任務(wù)的實(shí)現(xiàn)-多線程
實(shí)現(xiàn)多任務(wù)需要用到多線程,那么什么是線程呢?下面是對它的理解:
????1.一個(gè)程序運(yùn)行起來至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程
????2.處理器cpu分配給線程,即cpu真正運(yùn)行的是線程中的代碼
????3.分配cpu給線程時(shí),是通過時(shí)間片輪訓(xùn)方式進(jìn)行的
????4.進(jìn)程是操作系統(tǒng)分配程序執(zhí)行資源的單位,而線程是進(jìn)程的一個(gè)實(shí)體,
是CPU調(diào)度和分配的單位。
實(shí)現(xiàn)多線程的方式有2種方式:
1.python的thread模塊是比較底層的模塊,python的threading模塊是對thread做了一些包裝的,可以更加方便的被使用,通過threading模塊可以創(chuàng)建線程
# 引入threading線程模塊
import????threading,time?
def????download_music():
'''模擬下載歌曲,需要5秒鐘下載完成'''
????for? ? i? ? in? ? range(5):
?????time.sleep(1)# 休眠1秒
????print("---正在下載歌曲%d---"% i)
def????main():
????# 創(chuàng)建線程對象t1 ,target: 指向新開啟的線程要執(zhí)行的代碼
????t1 = threading.Thread(target=download_music)?
? ? t1.start()# 啟動線程,即線程開始執(zhí)行
if__name__ =='__main__':?
?????main()
2.繼承Thread類,創(chuàng)建一個(gè)新的class,將要執(zhí)行的代碼 寫到run函數(shù)里面
import????threading,time
# 自定義類,繼承threading.Thread
class????MyThread(threading.Thread):
????def????run(self):
????????for????i????in????range(5):
?????????????time.sleep(1)
????????????# name屬性中保存的是當(dāng)前線程的名字
? ? ? ? ? ? msg ="I'm "+ self.name +' @ '+ str(i)?
?????????????print(msg)
if__name__ =='__main__':
# 通過MyThread創(chuàng)建線程對象
????t1 = MyThread()
????# 開始執(zhí)行線程
????t1.start()
3.多線程的注意點(diǎn)
1. 線程何時(shí)開啟,何時(shí)結(jié)束
1.子線程何時(shí)開啟,何時(shí)運(yùn)行
? 當(dāng)調(diào)用thread.start()時(shí) 開啟線程,再運(yùn)行線程的代碼
2.子線程何時(shí)結(jié)束
? 子線程把target指向的函數(shù)中的語句執(zhí)行完畢后,或者線程中的run函數(shù)代碼執(zhí)行完畢后,立即結(jié)束當(dāng)前子線程
3.查看當(dāng)前線程數(shù)量
? 通過threading.enumerate()可枚舉當(dāng)前運(yùn)行的所有線程
4.主線程何時(shí)結(jié)束
? 所有子線程執(zhí)行完畢后,主線程才結(jié)束
2. 線程的執(zhí)行順序
多線程的創(chuàng)建與執(zhí)行都是無序的。
3.總結(jié)
1.每個(gè)線程默認(rèn)有一個(gè)名字,盡管上面的例子中沒有指定線程對象的name,但是python會自動為線程指定一個(gè)名字。
2.當(dāng)線程的run()方法結(jié)束時(shí)該線程完成。
3.無法控制線程調(diào)度程序,但可以通過別的方式來影響線程調(diào)度的方式。
4.共享全局變量
先來看一段代碼:
from????threading????import????Thread
import????time
g_nums = [11,22,33]
def? ? main():
????def????work1(nums):
????????nums.append(44)?
?????????print("----in work1---",nums)
????def????work2(nums):
????????#延時(shí)一會,保證t1線程中的事情做完
????????time.sleep(1)?
?????????print("----in work2---",nums)
if? ? __name__ == '__main__':
????t1 = Thread(target=work1, args=(g_nums,))
????t1.start()
????t2 = Thread(target=work2, args=(g_nums,))
????t2.start()
運(yùn)行結(jié)果:
----inwork1--- [11,22,33,44]
----inwork2--- [11,22,33,44]
總結(jié):
在一個(gè)進(jìn)程內(nèi)的所有線程共享全局變量,很方便在多個(gè)線程間共享數(shù)據(jù)
缺點(diǎn)就是,多線程對全局變量隨意遂改可能造成全局變量的混亂(即線程非安全)
5.多線程開發(fā)可能遇到的問題
????假設(shè)兩個(gè)線程t1和t2都要對全局變量g_num(默認(rèn)是0)進(jìn)行加1運(yùn)算,t1和t2都各對g_num加10次,g_num的最終的結(jié)果應(yīng)該為20。
????但是由于是多線程同時(shí)操作,有可能出現(xiàn)下面情況:
????????在g_num=0時(shí),t1取得g_num=0。此時(shí)系統(tǒng)把t1調(diào)度為”sleeping”狀態(tài),把t2轉(zhuǎn)換為”running”狀態(tài),t2也獲得g_num=0
????????然后t2對得到的值進(jìn)行加1并賦給g_num,使得g_num=1
????????然后系統(tǒng)又把t2調(diào)度為”sleeping”,把t1轉(zhuǎn)為”running”。線程t1又把它之前得到的0加1后賦值給g_num。
????????這樣導(dǎo)致雖然t1和t2都對g_num加1,但結(jié)果仍然是g_num=1。
????所以,如果多個(gè)線程同時(shí)對同一個(gè)全局變量操作,會出現(xiàn)資源競爭問題,從而數(shù)據(jù)結(jié)果會不正確,即會遇到線程安全問題
6.使用同步機(jī)制解決線程安全問題-互斥鎖
同步就是協(xié)同步調(diào),按預(yù)定的先后次序進(jìn)行運(yùn)行。如:你說完,我再說。
"同"字從字面上容易理解為一起動作,其實(shí)不是,"同"字應(yīng)是指協(xié)同、協(xié)助、互相配合。
如進(jìn)程、線程同步,可理解為進(jìn)程或線程A和B一塊配合,A執(zhí)行到一定程度時(shí)要依靠B的某個(gè)結(jié)果,于是停下來,示意B運(yùn)行;B執(zhí)行,再將結(jié)果給A;A再繼續(xù)操作。
在多線程編程里面,一些敏感數(shù)據(jù)不允許被多個(gè)線程同時(shí)訪問,此時(shí)就使用同步訪問技術(shù),保證數(shù)據(jù)在任何時(shí)刻,最多有一個(gè)線程訪問,以保證數(shù)據(jù)的正確性。
互斥鎖:
????當(dāng)多個(gè)線程幾乎同時(shí)修改某一個(gè)共享數(shù)據(jù)的時(shí)候,需要進(jìn)行同步控制
????線程同步能夠保證多個(gè)線程安全訪問競爭資源,最簡單的同步機(jī)制是引入互斥鎖。
????互斥鎖為資源引入一個(gè)狀態(tài):鎖定/非鎖定
????某個(gè)線程要更改共享數(shù)據(jù)時(shí),先將其鎖定,此時(shí)資源的狀態(tài)為“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態(tài)變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個(gè)線程進(jìn)行寫入操作,從而保證了多線程情況下數(shù)據(jù)的正確性。
threading模塊中定義了Lock類,可以方便的處理鎖定:
# 1.創(chuàng)建鎖
mutex = threading.Lock()
#2. 鎖定
mutex.acquire()
#3. 釋放
mutex.release()
注意:
如果這個(gè)鎖之前是沒有上鎖的,那么acquire不會堵塞
如果在調(diào)用acquire對這個(gè)鎖上鎖之前 它已經(jīng)被 其他線程上了鎖,那么此時(shí)acquire會堵塞,直到這個(gè)鎖被解鎖為止
示例:使用互斥鎖完成2個(gè)線程對同一個(gè)全局變量各加100萬次的操作
import????threading,time
g_num =0
def????test1(num):
????global????g_num
????for????i????in????range(num):
? ? ? ? mutex.acquire()# 上鎖
????????g_num +=1
????????mutex.release()# 解鎖
????print("---test1---g_num=%d"%g_num)
def????test2(num):
????global????g_num
????for????i????in????range(num):?
? ? ? ? mutex.acquire()# 上鎖
????????g_num +=1
????????mutex.release()# 解鎖
????print("---test2---g_num=%d"%g_num)
# 創(chuàng)建一個(gè)互斥鎖,默認(rèn)是未上鎖的狀態(tài)
mutex = threading.Lock()
# 創(chuàng)建2個(gè)線程,讓他們各自對g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
# 等待計(jì)算完成
while????len(threading.enumerate()) !=1:?
?????time.sleep(1)
print("2個(gè)線程對同一個(gè)全局變量操作之后的最終結(jié)果是:%s"% g_num)
運(yùn)行結(jié)果:
---test1---g_num=1909909
---test2---g_num=2000000
2個(gè)線程對同一個(gè)全局變量操作之后的最終結(jié)果是:2000000
可以看到最后的結(jié)果,加入互斥鎖后,其結(jié)果與預(yù)期相符。
上鎖解鎖過程:
????當(dāng)一個(gè)線程調(diào)用鎖的acquire()方法獲得鎖時(shí),鎖就進(jìn)入“l(fā)ocked”狀態(tài)。
????每次只有一個(gè)線程可以獲得鎖。如果此時(shí)另一個(gè)線程試圖獲得這個(gè)鎖,該線程就會變?yōu)椤癰locked”狀態(tài),稱為“阻塞”,直到擁有鎖的線程調(diào)用鎖的release()方法釋放鎖之后,鎖進(jìn)入“unlocked”狀態(tài)。
????線程調(diào)度程序從處于同步阻塞狀態(tài)的線程中選擇一個(gè)來獲得鎖,并使得該線程進(jìn)入運(yùn)行(running)狀態(tài)。
7.死鎖
在線程間共享多個(gè)資源的時(shí)候,如果兩個(gè)線程分別占有一部分資源并且同時(shí)等待對方的資源,就會造成死鎖。
盡管死鎖很少發(fā)生,但一旦發(fā)生就會造成應(yīng)用的停止響應(yīng)。下面看一個(gè)死鎖的例子
import????threading ,time? ?
printer_mutex = threading.Lock()# 打印機(jī)鎖
paper_mutext = threading.Lock()# 紙張鎖
class????ResumeThread(threading.Thread):
"""編寫個(gè)人簡歷任務(wù)的線程"""
????def????run(self):
????????print("ResumeThread:編寫個(gè)人簡歷任務(wù)")
????????# 使用打印機(jī)資源,先對打印機(jī)加鎖
????????printer_mutex.acquire()?
?????????print("--ResumeThread:正在使用打印機(jī)資源--")?
?????????time.sleep(1)# 休眠1秒
????????# 使用紙張耗材,先對紙張耗材加鎖
????????paper_mutext.acquire()?
????????print("--正在使用紙張資源--")?
?????????time.sleep(1)?
?????????paper_mutext.release()# 釋放紙張鎖
????????# 釋放打印機(jī)鎖
????????printer_mutex.release()
class????PaperListThread(threading.Thread):
"""盤點(diǎn)紙張耗材任務(wù)的線程"""
????def????run(self):
????????print("PaperListThread:盤點(diǎn)紙張耗材任務(wù)")
????????# 使用紙張耗材,先對紙張耗材加鎖
????????paper_mutext.acquire()?
?????????print("--PaperListThread:正在盤點(diǎn)紙張耗材--")
?????????time.sleep(1)# 休眠1秒
????????# 使用打印機(jī)資源,打印清單
????????printer_mutex.acquire()
?????????print("--正在使用打印機(jī)資源--")
?????????time.sleep(1)?
?????????printer_mutex.release()# 釋放打印機(jī)鎖
????????# 釋放紙張耗材鎖
????????paper_mutext.release()
if__name__ =='__main__':
? ? ?t1 = ResumeThread()
?????t2 = PaperListThread()
?????t1.start()?
?????t2.start()

從運(yùn)行結(jié)果可以看出,兩個(gè)線程都在等待對方釋放資源,造成了死鎖。
避免死鎖的方式:
????1.程序設(shè)計(jì)時(shí)要盡量避免
? ? 2.添加超時(shí)時(shí)間等