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