線程,有時被稱爲輕量進程,是程序執行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程不擁有私有的系統資源,但它可與同屬一個進程的其它線程共享進程所擁有的所有資源。一個線程能夠建立和撤消另外一個線程,同一進程中的多個線程之間能夠併發執行。python
線程是程序中一個單一的順序控制流程。進程內有一個相對獨立的、可調度的執行單元,是系統獨立調度和分派CPU的基本單位指令運行時的程序的調度單位。在單個程序中同時運行多個線程完成不一樣的工做,稱爲多線程。Python多線程用於I/O操做密集型的任務,如SocketServer網絡併發,網絡爬蟲。數據庫
現代處理器都是多核的,幾核處理器只能同時處理幾個線程,多線程執行程序看起來是同時進行,其實是CPU在多個線程之間快速切換執行,這中間就涉及到上下問切換,所謂的上下文切換就是指一個線程Thread被分配的時間片用完了以後,線程的信息被保存起來,CPU執行另外的線程,再到CPU讀取線程Thread的信息並繼續執行Thread的過程。編程
線程模塊安全
Python的標準庫提供了兩個模塊:_thread和threading。_thread 提供了低級別的、原始的線程以及一個簡單的互斥鎖,它相比於 threading 模塊的功能仍是比較有限的。Threading模塊是_thread模塊的替代,在實際的開發中,絕大多數狀況下仍是使用高級模塊threading,所以本書着重介紹threading高級模塊的使用。網絡
Python建立Thread對象語法以下:多線程
import threading
threading.Thread(target=None, name=None, args=())
複製代碼
主要參數說明:併發
Python中實現多線程有兩種方式:函數式建立線程和建立線程類。dom
第一種建立線程方式:函數
建立線程的時候,只須要傳入一個執行函數和函數的參數便可完成threading.Thread實例的建立。下面的例子使用Thread類來產生2個子線程,而後啓動2個子線程並等待其結束,學習
''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading
import time,random,math
# idx 循環次數
def printNum(idx):
for num in range(idx ):
#打印當前運行的線程名字
print("{0}\tnum={1}".format(threading.current_thread().getName(), num) )
delay = math.ceil(random.random() * 2)
time.sleep(delay)
if __name__ == '__main__':
th1 = threading.Thread(target=printNum, args=(2,),name="thread1" )
th2 = threading.Thread(target=printNum, args=(3,),name="thread2" )
#啓動2個線程
th1.start()
th2.start()
#等待至線程停止
th1.join()
th2.join()
print("{0} 線程結束".format(threading.current_thread().getName()))
複製代碼
運行腳本獲得如下結果。
thread1 num=0
thread2 num=0
thread1 num=1
thread2 num=1
thread2 num=2
複製代碼
MainThread 線程結束
運行腳本默認會啓動一個線程,把該線程稱爲主線程,主線程有能夠啓動新的線程,Python的threading模塊有個current_thread()函數,它將返回當前線程的示例。從當前線程的示例能夠得到前運行線程名字,核心代碼以下。
threading.current_thread().getName()
複製代碼
啓動一個線程就是把一個函數和參數傳入並建立Thread實例,而後調用start()開始執行
th1 = threading.Thread(target=printNum, args=(2,),name="thread1" )
th1.start()
複製代碼
從返回結果能夠看出主線程示例的名字叫MainThread,子線程的名字在建立時指定,本例建立了2個子線程,名字叫thread1和thread2。若是沒有給線程起名字,Python就自動給線程命名爲Thread-1,Thread-2…等等。在本例中定義了線程函數printNum(),打印idx次記錄後退出,每次打印使用time.sleep()讓程序休眠一段時間。
第二種建立線程方式:建立線程類
直接建立threading.Thread的子類來建立一個線程對象,實現多線程。經過繼承Thread類,並重寫Thread類的run()方法,在run()方法中定義具體要執行的任務。在Thread類中,提供了一個start()方法用於啓動新進程,線程啓動後會自動調用run()方法。
''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading
import time,random,math
class MutliThread(threading.Thread):
def __init__(self, threadName,num):
threading.Thread.__init__(self)
self.name = threadName
self.num = num
def run(self):
for i in range(self.num):
print("{0} i={1}".format(threading.current_thread().getName(), i))
delay = math.ceil(random.random() * 2)
time.sleep(delay)
if __name__ == '__main__':
thr1 = MutliThread("thread1",3)
thr2 = MutliThread("thread2",2)
# 啓動線程
thr1.start()
thr2.start()
# 等待至線程停止
thr1.join()
thr2.join()
print("{0} 線程結束".format(threading.current_thread().getName()))
運行腳本獲得如下結果。
thread1 i=0
thread2 i=0
thread1 i=1
thread2 i=1
thread1 i=2
複製代碼
MainThread 線程結束 從返回結果能夠看出,經過建立Thread類來產生2個線程對象thr1和thr2,重寫Thread類的run()函數,把業務邏輯放入其中,經過調用線程對象的start()方法啓動線程。經過調用線程對象的join()函數,等待該線程完成,在繼續下面的操做。
在本例中,主線程MainThread等待子線程thread1和thread2線程運行結束後才輸出」 MainThread 線程結束」。若是子線程thread1和thread2不調用join()函數,那麼主線程MainThread和2個子線程是並行執行任務的,2個子線程加上join()函數後,程序就變成順序執行了。因此子線程用到join()的時候,一般都是主線程等到其餘多個子線程執行完畢後再繼續執行,其餘的多個子線程並不須要互相等待。
守護線程
在線程模塊中,使用子線程對象用到join()函數,主線程須要依賴子線程執行完畢後才繼續執行代碼。若是子線程不使用join()函數,主線程和子線程是並行運行的,沒有依賴關係,主線程執行了,子線程也在執行。
在多線程開發中,若是子線程設定爲了守護線程,守護線程會等待主線程運行完畢後被銷燬。一個主線程能夠設置多個守護線程,守護線程運行的前提是,主線程必須存在,若是主線程不存在了,守護線程會被銷燬。
在本例中建立1個主線程3個子線程,讓主線程和子線程並行執行。內容以下。
''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading, time
def run(taskName):
print("任務:", taskName)
time.sleep(2)
print("{0} 任務執行完畢".format(taskName)) # 查看每一個子線程
if __name__ == '__main__':
start_time = time.time()
for i in range(3):
thr = threading.Thread(target=run, args=("task-{0}".format(i),))
# 把子線程設置爲守護線程
thr.setDaemon(True)
thr.start()
# 查看主線程和當前活動的全部線程數
print("{0}線程結束,當線程數量={1}".format( threading.current_thread().getName(), threading.active_count()))
print("消耗時間:", time.time() - start_time)
運行腳本獲得如下結果:
任務: task-0
任務: task-1
任務: task-2
MainThread線程結束,當線程數量=4
消耗時間: 0.0009751319885253906
task-2 任務執行完畢
task-0 任務執行完畢
task-1 任務執行完畢
複製代碼
從返回結果能夠看出,當前的線程個數是4,線程個數=主線程數 + 子線程數,在本例中有1個主線程和3個子線程。主線程執行完畢後,等待子線程執行完畢,程序纔會退出。
在本例的基礎上,把全部的子線程都設置爲守護線程。子線程變成守護線程後,只要主線程執行完畢,程序無論子線程有沒有執行完畢,程序都會退出。使用線程對象的setDaemon(True)函數來設置守護線程。
''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading, time
def run(taskName):
print("任務:", taskName)
time.sleep(2)
print("{0} 任務執行完畢".format(taskName))
if __name__ == '__main__':
start_time = time.time()
for i in range(3):
thr = threading.Thread(target=run, args=("task-{0}".format(i),))
# 把子線程設置爲守護線程,在啓動線程前設置
thr.setDaemon(True)
thr.start()
# 查看主線程和當前活動的全部線程數
thrName = threading.current_thread().getName()
thrCount = threading.active_count()
print("{0}線程結束,當線程數量={1}".format(thrName, thrCount))
print("消耗時間:", time.time() - start_time)
運行腳本獲得如下結果。
任務: task-0
任務: task-1
任務: task-2
MainThread線程結束,當線程數量=4
消耗時間: 0.0010023117065429688
複製代碼
從本例的返回結果能夠看出,主線程執行完畢後,程序不會等待守護線程執行完畢後就退出了。設置線程對象爲守護線程,必定要在線程對象調用start()函數前設置。
多線程的鎖機制
多線程編程訪問共享變量時會出現問題,可是多進程編程訪問共享變量不會出現問題。由於多進程中,同一個變量各自有一份拷貝存在於每一個進程中,互不影響,而多線程中,全部變量都由全部線程共享。 多個進程之間對內存中的變量不會產生衝突,一個進程由多個線程組成,多線程對內存中的變量進行共享時會產生影響,因此就產生了死鎖問題,怎麼解決死鎖問題是本節主要介紹的內容。
一、變量的做用域
通常在函數體外定義的變量稱爲全局變量,在函數內部定義的變量稱爲局部變量。全局變量全部做用域均可讀,局部變量只能在本函數可讀。函數在讀取變量時,優先讀取函數自己自有的局部變量,再去讀全局變量。 內容以下。
''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
# 全局變量
balance = 1
def change():
# 定義全局變量
global balance
balance = 100
# 定義局部變量
num = 20
print("change() balance={0}".format(balance) )
if __name__ == "__main__" :
change()
print("修改後的 balance={0}".format(balance) )
運行腳本獲得如下結果。
change() balance=100
修改後的 balance=100
複製代碼
若是註釋掉change()函數裏的 global
v1,那麼獲得的返回值是。
change() balance=100
修改後的 balance=1
複製代碼
在本例中在change()函數外定義的變量balance是全局變量,在change()函數內定義的變量num是局部變量,全局變量默認是可讀的,能夠在任何函數中使用,若是須要改變全局變量的值,須要在函數內部使用global定義全局變量,本例中在change()函數內部使用global定義全局變量balance,在函數裏就能夠改變全局變量了。
在函數裏可使用全局變量,可是在函數裏不能改變全局變量。想實現多個線程共享變量,須要使用全局變量。在方法里加上全局關鍵字 global定義全局變量,多線程才能夠修改全局變量來共享變量。
二、多線程中的鎖
多線程同時修改全局變量時會出現數據安全問題,線程不安全就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據。在本例中咱們生成2個線程同時修改change()函數裏的全局變量balance時,會出現數據不一致問題。
本案例文件名爲PythonFullStack\Chapter03\threadDemo03.py,內容以下。
import threading
balance = 100
def change(num, counter):
global balance
for i in range(counter):
balance += num
balance -= num
if balance != 100:
# 若是輸出這句話,說明線程不安全
print("balance=%d" % balance)
break
if __name__ == "__main__":
thr1 = threading.Thread(target=change,args=(100,500000),name='t1')
thr2 = threading.Thread(target=change,args=(100,500000),name='t2')
thr1.start()
thr2.start()
thr1.join()
thr2.join()
print("{0} 線程結束".format(threading.current_thread().getName()))
運行以上腳本,當2個線程運行次數達到500000次時,會出現如下結果。
balance=200
MainThread 線程結束
複製代碼
在本例中定義了一個全局變量balance,初始值爲100,當啓動2個線程後,先加後減,理論上balance應該爲100。線程的調度是由操做系統決定的,當線程t1和t2交替執行時,只要循環次數足夠多,balance結果就不必定是100了。從結果能夠看出,在本例中線程t1和t2同時修改全局變量balance時,會出現數據不一致問題。
注意
在多線程狀況下,全部的全局變量有全部線程共享。因此,任何一個變量均可以被任何一個線程修改,所以,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。
在多線程狀況下,使用全局變量並不會共享數據,會出現線程安全問題。線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致
在單線程運行時沒有代碼安全問題。寫多線程程序時,生成一個線程並不表明多線程。在多線程狀況下,纔會出現安全問題。
針對線程安全問題,須要使用」互斥鎖」,就像數據庫裏操縱數據同樣,也須要使用鎖機制。某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲「鎖定」,其餘線程不能更改;直到該線程釋放資源,將資源的狀態變成「非鎖定」,其餘的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操做,從而保證了多線程狀況下數據的正確性。
互斥鎖的核心代碼以下:
# 建立鎖
mutex = threading.Lock()
# 鎖定
mutex.acquire()
# 釋放
mutex.release()
複製代碼
若是要確保balance計算正確,使用threading.Lock()來建立鎖對象lock,把 lock.acquire()和lock.release()加在同步代碼塊裏,本例的同步代碼塊就是對全局變量balance進行先加後減操做。
當某個線程執行change()函數時,經過lock.acquire()獲取鎖,那麼其餘線程就不能執行同步代碼塊了,只能等待知道鎖被釋放了,得到鎖才能執行同步代碼塊。因爲鎖只有一個,不管多少線程,同一個時刻最多隻有一個線程持有該鎖,因此修改全局變量balance不會產生衝突。改良後的代碼內容以下。
''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
import threading
balance = 100
lock = threading.Lock()
def change(num, counter):
global balance
for i in range(counter):
# 先要獲取鎖
lock.acquire()
balance += num
balance -= num
# 釋放鎖
lock.release()
if balance != 100:
# 若是輸出這句話,說明線程不安全
print("balance=%d" % balance)
break
if __name__ == "__main__":
thr1 = threading.Thread(target=change,args=(100,500000),name='t1')
thr2 = threading.Thread(target=change,args=(100,500000),name='t2')
thr1.start()
thr2.start()
thr1.join()
thr2.join()
print("{0} 線程結束".format(threading.current_thread().getName()))
複製代碼
在本例中2個線程同時運行lock.acquire()時,只有一個線程能成功的獲取鎖,而後執行代碼,其餘線程就繼續等待直到得到鎖位置。得到鎖的線程用完後必定要釋放鎖,不然其餘線程就會一直等待下去,成爲死線程。
在運行上面腳本就不會產生輸出信息,證實代碼是安全的。把 lock.acquire()和lock.release()加在同步代碼塊裏,還要注意鎖的力度不要加的太大了。第一個線程只有運行完了,第二個線程才能運行,因此鎖要在須要同步代碼里加上。