1. 多線程-threading
Python的標(biāo)準(zhǔn)庫(kù)提供了兩個(gè)模塊:_thread和threading,_thread是低級(jí)模塊,threading是高級(jí)模塊,對(duì)_thread進(jìn)行了封裝。絕大多數(shù)情況下,我們只需要使用threading這個(gè)高級(jí)模塊。
啟動(dòng)一個(gè)線程就是把一個(gè)函數(shù)傳入并創(chuàng)建Thread實(shí)例,然后調(diào)用start()開始執(zhí)行:
- 單線程執(zhí)行
import time
def saySorry():
print("親愛的,我錯(cuò)了,我能吃飯了嗎?%s"%time.ctime())
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
saySorry()
- 運(yùn)行結(jié)果
>>>
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:33 2017
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:34 2017
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:35 2017
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:36 2017
親愛的,我錯(cuò)了,我能吃飯了嗎?Mon Jun 12 17:08:37 2017
- 多線程執(zhí)行
import time
import threading
def saySorry():
print("親愛的,我錯(cuò)了,我能吃飯了嗎? name=%s, %s"%(threading.current_thread().name,time.ctime()))
time.sleep(1)
if __name__ == "__main__":
print(threading.current_thread().name)
for i in range(5):
t1 = threading.Thread(target=saySorry)
t1.start()
- 運(yùn)行結(jié)果
>>>
MainThread
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-1, Mon Jun 12 19:15:02 2017
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-2, Mon Jun 12 19:15:02 2017
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-3, Mon Jun 12 19:15:02 2017
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-4, Mon Jun 12 19:15:02 2017
親愛的,我錯(cuò)了,我能吃飯了嗎? name=Thread-5, Mon Jun 12 19:15:02 2017
#多線程的時(shí)候可以看出來(lái)是同時(shí)執(zhí)行的
說(shuō)明
- 可以明顯看出使用了多線程并發(fā)的操作,花費(fèi)時(shí)間要短很多
- 創(chuàng)建好的線程,需要調(diào)用start()方法來(lái)啟動(dòng)
由于任何進(jìn)程默認(rèn)就會(huì)啟動(dòng)一個(gè)線程,我們把該線程稱為主線程,主線程又可以啟動(dòng)新的線程,Python的threading模塊有個(gè)current_thread()函數(shù),它永遠(yuǎn)返回當(dāng)前線程的實(shí)例。主線程實(shí)例的名字叫MainThread,子線程的名字在創(chuàng)建時(shí)指定,這里我們沒有指定。名字僅僅在打印時(shí)用來(lái)顯示,完全沒有其他意義,如果不起名字Python就自動(dòng)給線程命名為Thread-1,Thread-2……
主線程會(huì)等待所有的子線程結(jié)束后才結(jié)束
查看線程數(shù)量
length = len(threading.enumerate())
print('當(dāng)前運(yùn)行的線程數(shù)為:%d' % length)
2. threading注意點(diǎn)
1.2.2.1線程執(zhí)行代碼的封裝
通過(guò)上一小節(jié),能夠看出,通過(guò)使用threading模塊能完成多任務(wù)的程序開發(fā),為了讓每個(gè)線程的封裝性更完美,所以使用threading模塊時(shí),往往會(huì)定義一個(gè)新的子類class,只要繼承threading.Thread就可以了,然后重寫run方法
示例如下:
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm " + self.name + ' @ ' + str(i) # name屬性中保存的是當(dāng)前線程的名字
print(msg)
if __name__ == '__main__':
t = MyThread()
t.start()
說(shuō)明
python的threading.Thread類有一個(gè)run方法,用于定義線程的功能函數(shù),可以在自己的線程類中覆蓋該方法。而創(chuàng)建自己的線程實(shí)例后,通過(guò)Thread類的start方法,可以啟動(dòng)該線程,交給python虛擬機(jī)進(jìn)行調(diào)度,當(dāng)該線程獲得執(zhí)行的機(jī)會(huì)時(shí),就會(huì)調(diào)用run方法執(zhí)行線程。
- 線程的執(zhí)行順序
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm " + self.name + ' @ ' + str(i)
print(msg)
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
- 運(yùn)行結(jié)果(運(yùn)行的結(jié)果可能不一樣,但是大體是一致的)
I'm Thread-2 @ 0
I'm Thread-1 @ 0
I'm Thread-3 @ 0
I'm Thread-4 @ 0
I'm Thread-5 @ 0
I'm Thread-1 @ 1
I'm Thread-2 @ 1
I'm Thread-3 @ 1
I'm Thread-4 @ 1
I'm Thread-5 @ 1
I'm Thread-2 @ 2
I'm Thread-1 @ 2
I'm Thread-3 @ 2
I'm Thread-4 @ 2
I'm Thread-5 @ 2
說(shuō)明
從代碼和執(zhí)行結(jié)果我們可以看出,多線程程序的執(zhí)行順序是不確定的。當(dāng)執(zhí)行到sleep語(yǔ)句時(shí),線程將被阻塞(Blocked),到sleep結(jié)束后,線程進(jìn)入就緒(Runnable)狀態(tài),等待調(diào)度。而線程調(diào)度將自行選擇一個(gè)線程執(zhí)行。上面的代碼中只能保證每個(gè)線程都運(yùn)行完整個(gè)run函數(shù),但是線程的啟動(dòng)順序、run函數(shù)中每次循環(huán)的執(zhí)行順序都不能確定。
總結(jié)
- 每個(gè)線程一定會(huì)有一個(gè)名字,盡管上面的例子中沒有指定線程對(duì)象的name,但是python會(huì)自動(dòng)為線程指定一個(gè)名字。
- 當(dāng)線程的run()方法結(jié)束時(shí)該線程完成。
- 無(wú)法控制線程調(diào)度程序,但可以通過(guò)別的方式來(lái)影響線程調(diào)度的方式。
- 線程的幾種狀態(tài)
3. 多線程-共享全局變量
- 在一個(gè)進(jìn)程內(nèi)的所有線程共享全局變量,能夠在不適用其他方式的前提下完成多線程之間的數(shù)據(jù)共享(這點(diǎn)要比多進(jìn)程要好)
- 缺點(diǎn)就是,線程是對(duì)全局變量隨意遂改可能造成多線程之間對(duì)全局變量的混亂(即線程非安全)
4. 進(jìn)程VS線程
定義
- 進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位.
- 線程是進(jìn)程的一個(gè)實(shí)體,是CPU調(diào)度和分派的基本單位,它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位.線程自己基本上不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計(jì)數(shù)器,一組寄存器和棧),但是它可與同屬一個(gè)進(jìn)程的其他的線程共享進(jìn)程所擁有的全部資源.
區(qū)別
- 一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程.
- 線程的劃分尺度小于進(jìn)程(資源比進(jìn)程少),使得多線程程序的并發(fā)性高。
- 進(jìn)程在執(zhí)行過(guò)程中擁有獨(dú)立的內(nèi)存單元,而多個(gè)線程共享內(nèi)存,從而極大地提高了程序的運(yùn)行效率
- 線程不能夠獨(dú)立執(zhí)行,必須依存在進(jìn)程中
優(yōu)缺點(diǎn)
- 線程和進(jìn)程在使用上各有優(yōu)缺點(diǎn):線程執(zhí)行開銷小,但不利于資源的管理和保護(hù);而進(jìn)程正相反。
5. 同步的概念
- 什么是同步
同步就是協(xié)同步調(diào),按預(yù)定的先后次序進(jìn)行運(yùn)行。如:你說(shuō)完,我再說(shuō)。
"同"字從字面上容易理解為一起動(dòng)作
其實(shí)不是,"同"字應(yīng)是指協(xié)同、協(xié)助、互相配合。
如進(jìn)程、線程同步,可理解為進(jìn)程或線程A和B一塊配合,A執(zhí)行到一定程度時(shí)要依靠B的某個(gè)結(jié)果,于是停下來(lái),示意B運(yùn)行;B依言執(zhí)行,再將結(jié)果給A;A再繼續(xù)操作。 - 多線程的優(yōu)勢(shì)在于可以同時(shí)運(yùn)行多個(gè)任務(wù)(至少感覺起來(lái)是這樣)。但是當(dāng)線程需要共享數(shù)據(jù)時(shí),可能存在數(shù)據(jù)不同步的問(wèn)題。
考慮這樣一種情況:一個(gè)列表里所有元素都是0,線程"set"從后向前把所有元素改成1,而線程"print"負(fù)責(zé)從前往后讀取列表并打印。
那么,可能線程"set"開始改的時(shí)候,線程"print"便來(lái)打印列表了,輸出就成了一半0一半1,這就是數(shù)據(jù)的不同步。為了避免這種情況,引入了鎖的概念。
6. 互斥鎖
threading模塊中定義了Lock類,可以方便的處理鎖定:
import threading
#創(chuàng)建鎖
myLock = threading.Lock()
print(myLock)
print('1...')
#鎖住,如果此鎖,已經(jīng)鎖了,如果再鎖,會(huì)阻塞,直到開鎖了,才能再鎖
#myLock.acquire()
myLock.acquire()
print('2...')
#開鎖,如果鎖住了,可以開鎖。如果沒鎖,直接開,報(bào)錯(cuò)
myLock.release()
print('3...')
myLock.acquire()
print('4...')
- 運(yùn)行結(jié)果
>>>
<unlocked _thread.lock object at 0x0000000000AEBC60>
1...
2...
3...
4...
其中,鎖定方法acquire可以有一個(gè)blocking參數(shù)。
- 如果設(shè)定blocking為True,則當(dāng)前線程會(huì)堵塞,直到獲取到這個(gè)鎖為止(如果沒有指定,那么默認(rèn)為True)
- 如果設(shè)定blocking為False,則當(dāng)前線程不會(huì)堵塞
例子
import threading
import time
num = 0
myLock=threading.Lock()
def fun1():
global num
for i in range(1000000):
myLock.acquire()
num +=1
myLock.release()
print('fun1...num=%d,%s'%(num,time.ctime()))
def fun2():
global num
for i in range(10000000):
myLock.acquire()
num += 1
myLock.release()
#time.sleep(2)
print('fun2...num=%d,%s'%(num,time.ctime()))
def main():
t1 = threading.Thread(target=fun1)
t2 = threading.Thread(target=fun2)
t1.start()
#time.sleep(2)
t2.start()
print('num=%d,%s'%(num,time.ctime()))
if __name__ == '__main__':
main()
- 運(yùn)行結(jié)果
num=22642,Mon Jun 12 20:29:23 2017
fun1...num=2060762,Mon Jun 12 20:29:25 2017
fun2...num=11000000,Mon Jun 12 20:29:29 2017
在打印fun1的時(shí)候,cpu權(quán)限切換到fun2,此時(shí)num已經(jīng)在瘋狂運(yùn)算了,所以打印的時(shí)候不是1000000.如果沒有互斥鎖的話,最后這個(gè)fun2打印出來(lái)的不會(huì)是這個(gè)值。
例子2:簡(jiǎn)單的售票,同步鎖完成售票
import threading
import time
import os
def doChore(): # 作為間隔 每次調(diào)用間隔0.5s
time.sleep(0.5)
def booth(tid):
global i
global lock
while True:
lock.acquire() # 得到一個(gè)鎖,鎖定
if i != 0:
i = i - 1 # 售票 售出一張減少一張
print(tid, ':now left:', i) # 剩下的票數(shù)
doChore()
else:
print("Thread_id", tid, " No more tickets")
os._exit(0) # 票售完 退出程序
lock.release() # 釋放鎖
doChore()
#全局變量
i = 15 # 初始化票數(shù)
lock = threading.Lock() # 創(chuàng)建鎖
def main():
# 總共設(shè)置了3個(gè)線程
for k in range(3):
# 創(chuàng)建線程; Python使用threading.Thread對(duì)象來(lái)代表線程
new_thread = threading.Thread(target=booth, args=(k,))
# 調(diào)用start()方法啟動(dòng)線程
new_thread.start()
if __name__ == '__main__':
main()
- 運(yùn)行結(jié)果
0 :now left: 14
1 :now left: 13
2 :now left: 12
0 :now left: 11
1 :now left: 10
2 :now left: 9
0 :now left: 8
1 :now left: 7
0 :now left: 6
2 :now left: 5
1 :now left: 4
0 :now left: 3
2 :now left: 2
1 :now left: 1
0 :now left: 0
Thread_id 2 No more tickets
總結(jié)
鎖的好處:
- 確保了某段關(guān)鍵代碼只能由一個(gè)線程從頭到尾完整地執(zhí)行
鎖的壞處: - 阻止了多線程并發(fā)執(zhí)行,包含鎖的某段代碼實(shí)際上只能以單線程模式執(zhí)行,效率就大大地下降了
- 由于可以存在多個(gè)鎖,不同的線程持有不同的鎖,并試圖獲取對(duì)方持有的鎖時(shí),可能會(huì)造成死鎖
7. 多線程-非共享數(shù)據(jù)
對(duì)于全局變量,在多線程中要格外小心,否則容易造成數(shù)據(jù)錯(cuò)亂的情況發(fā)生
在多線程開發(fā)中,全局變量是多個(gè)線程都共享的數(shù)據(jù),而局部變量等是各自線程的,是非共享的
8. 死鎖
- 在線程間共享多個(gè)資源的時(shí)候,如果兩個(gè)線程分別占有一部分資源并且同時(shí)等待對(duì)方的資源,就會(huì)造成死鎖。
- 盡管死鎖很少發(fā)生,但一旦發(fā)生就會(huì)造成應(yīng)用的停止響應(yīng)。
避免死鎖
- 程序設(shè)計(jì)時(shí)要盡量避免(銀行家算法)
- 添加超時(shí)時(shí)間等
9. 同步應(yīng)用
from threading import Thread, Lock
from time import sleep
'''
等待鎖的打開:阻塞-喚醒 機(jī)制
還有一種實(shí)現(xiàn)形式:輪循,效率低
'''
class Task1(Thread):
def run(self):
while True:
if lock1.acquire():
print("------Task 1 -----")
sleep(0.5)
lock2.release()
class Task2(Thread):
def run(self):
while True:
if lock2.acquire():
print("------Task 2 -----")
sleep(0.5)
lock3.release()
class Task3(Thread):
def run(self):
while True:
if lock3.acquire():
print("------Task 3 -----")
sleep(0.5)
lock1.release()
# 使用Lock創(chuàng)建出的鎖默認(rèn)沒有“鎖上”
lock1 = Lock()
# 創(chuàng)建另外一把鎖,并且“鎖上”
lock2 = Lock()
lock2.acquire()
# 創(chuàng)建另外一把鎖,并且“鎖上”
lock3 = Lock()
lock3.acquire()
if __name__ == '__main__':
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()
- 運(yùn)行結(jié)果
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
------Task 1 -----
------Task 2 -----
------Task 3 -----
...省略...
總結(jié)
- 可以使用互斥鎖完成多個(gè)任務(wù),有序的進(jìn)程工作,這就是線程的同步
10. 生產(chǎn)者與消費(fèi)者模式
- 隊(duì)列
先進(jìn)先出 - 棧
先進(jìn)后出
Python的Queue模塊中提供了同步的、線程安全的隊(duì)列類,包括FIFO(先入先出)隊(duì)列Queue,LIFO(后入先出)隊(duì)列LifoQueue,和優(yōu)先級(jí)隊(duì)列PriorityQueue。這些隊(duì)列都實(shí)現(xiàn)了鎖原語(yǔ)(可以理解為原子操作,即要么不做,要么就做完),能夠在多線程中直接使用。可以使用隊(duì)列來(lái)實(shí)現(xiàn)線程間的同步。
用FIFO隊(duì)列實(shí)現(xiàn)上述生產(chǎn)者與消費(fèi)者問(wèn)題的代碼如下:
'''
隊(duì)列:
1、進(jìn)程之間的通信: q = multiprocessing.Queue()
2、進(jìn)程池之間的通信: q = multiprocessing.Manager().Queue()
3、線程之間的通信: q = queue.Queue()
'''
import threading
import time
# python2中
# from Queue import Queue
# python3中
from queue import Queue
class Producer(threading.Thread):
def run(self):
global queue
count = 0
while True:
if queue.qsize() < 1000:
for i in range(100):
count = count + 1
msg = '生成產(chǎn)品' + str(count)
queue.put(msg)
print(msg)
time.sleep(1)
class Consumer(threading.Thread):
def run(self):
global queue
while True:
if queue.qsize() > 100:
for i in range(3):
msg = self.name + '消費(fèi)了 ' + queue.get()
print(msg)
time.sleep(1)
#全局變量
queue = Queue()
if __name__ == '__main__':
for i in range(500):
queue.put('初始產(chǎn)品' + str(i))
for i in range(2):
p = Producer()
p.start()
for i in range(5):
c = Consumer()
c.start()
Queue的說(shuō)明
1.對(duì)于Queue,在多線程通信之間扮演重要的角色
2.添加數(shù)據(jù)到隊(duì)列中,使用put()方法
3.從隊(duì)列中取數(shù)據(jù),使用get()方法
4.判斷隊(duì)列中是否還有數(shù)據(jù),使用qsize()方法
11. ThreadLocal
在多線程環(huán)境下,每個(gè)線程都有自己的數(shù)據(jù)。一個(gè)線程使用自己的局部變量比使用全局變量好,因?yàn)榫植孔兞恐挥芯€程自己能看見,不會(huì)影響其他線程,而全局變量的修改必須加鎖。
- 使用ThreadLocal的方法
ThreadLocal應(yīng)運(yùn)而生,不用查找dict,ThreadLocal幫你自動(dòng)做這件事:
'''
threadlocal就有倆功能:
1、將各自的局部變量綁定到各自的線程中
2、局部變量可以傳遞了,而且并沒有變成形參
'''
import threading
# 創(chuàng)建全局ThreadLocal對(duì)象:
local_school = threading.local()
def process_student():
# 獲取當(dāng)前線程關(guān)聯(lián)的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 綁定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('yongGe',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()
- 運(yùn)行結(jié)果
Hello, yongGe (in Thread-A)
Hello, 老王 (in Thread-B)
小結(jié)
一個(gè)ThreadLocal變量雖然是全局變量,但每個(gè)線程都只能讀寫自己線程的獨(dú)立副本,互不干擾。ThreadLocal解決了參數(shù)在一個(gè)線程中各個(gè)函數(shù)之間互相傳遞的問(wèn)題
12. 異步
- 同步調(diào)用就是你 喊 你朋友吃飯 ,你朋友在忙 ,你就一直在那等,等你朋友忙完了 ,你們一起去
- 異步調(diào)用就是你 喊 你朋友吃飯 ,你朋友說(shuō)知道了 ,待會(huì)忙完去找你 ,你就去做別的了。
- 代碼
from multiprocessing import Pool
import time
import os
def test():
print("---進(jìn)程池中的進(jìn)程---pid=%d,ppid=%d--" % (os.getpid(), os.getppid()))
for i in range(3):
print("----%d---" % i)
time.sleep(1)
return "老王"
def test2(args):
print('1...')
time.sleep(10)
print("---callback func--pid=%d" % os.getpid())
print("---callback func--args=%s" % args)
print('2...')
if __name__ == '__main__':
pool = Pool(3)
#callback表示前面的func方法執(zhí)行完,再執(zhí)行callback,并且可以獲取func的返回值作為callback的參數(shù)
pool.apply_async(func=test, callback=test2)
#pool.apply_async(func=test)
#模擬主進(jìn)程在做任務(wù)
time.sleep(5)
print("----主進(jìn)程-pid=%d.....服務(wù)器是不關(guān)閉的----" % os.getpid())
- 運(yùn)行結(jié)果
---進(jìn)程池中的進(jìn)程---pid=8808,ppid=8072--
----0---
----1---
----2---
1...
----主進(jìn)程-pid=8072.....服務(wù)器是不關(guān)閉的----
---callback func--pid=8072
---callback func--args=老王
2...