一次性付費進群,長期免費索取教程,沒有付費教程。
教程列表見微信公眾號底部菜單
進微信群回復公眾號:微信群;QQ群:460500587
微信公眾號:計算機與網絡安全
ID:Computer-network
不管哪種程式語言,多線程都是必不可少的。這種提高工作效率的神器,怎麼重視都不過分。多線程,就是將多個線性順序執行的過程變成並行運行。並行的數量越多,效率就越高。如果需要放空一個水池的水,打開多個放水孔的效率顯然要比打開一個放水孔的效率高。這種做法是典型的以資源換時間。
首先設計一個簡單的程序threadingOrderRun.py,打開Putty連接到Linux,執行命令:threadingOrderRun.py的代碼如下:1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6
7 def showName(name):
8 nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))
9 print('My name is function-%s, now time: %s ' %(name, nowTime))
10 time.sleep(1)
11
12
13 if __name__ == '__main__':
14 for i in range(20): #沒有開多線程的情況下,執行20次操作
15 showName(i)
這個函數的功能很簡單,就是從列表中提取名字作為showName的參數,然後顯示名字和當前時間。最後的time.sleep(1)是因為這個過程太快了,所以休眠1秒以便於觀察。這是一種理所當然的順序操作方法,但理所當然的操作並不是最有效率的操作。這種方法完全就是線性操作,從列表中取出一個元素,就用函數處理一個元素。現在運行程序試試,執行命令:
time python3 threadingOrderRun.py圖1 線性順序執行
從圖1中可以看出,該程序執行時間為20.070秒。如果列表比較小,那還可以忍受。如果這個列表比較大呢?1000個元素至少得1000秒,那就完全無法接受了。明明計算機有多餘的資源,卻花費了這麼多的時間。完全可以利用資源來換取時間,用多線程操作。在同一時間內運行多個線程(事實上線程並不是同時運行的,是基於時間片輪轉的方式執行。但對於操作者來說,跟同時運行並沒有什麼區別)。這樣雖然佔用了一定的資源,但大大地節省了時間。畢竟絕大多數的情況下都是希望時間優先的。在Python中,多線程的模塊是threading模塊。如果要完全深入了解threading模塊,恐怕得好好地讀一下Python的說明文檔。如果只是要使用,那就很簡單了。這裡需要注意的是,使用threading模塊進行多線程操作有兩種方法。一種是以函數的形式調用,一種是以類的方式調用。先以函數的形式使用多線程。打開Putty連接到Linux,執行命令:
vi threadingOfFunction.py1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6 import threading
7
8 def showName(threadNum ,name):
9 nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))
10 print('I am thread-%d ,My name is function-%s, now time: %s '%(threadNum, name, nowTime))
11 time.sleep(1)
12
13 if __name__ == '__main__':
14 print('I am main ...')
15 names = range(20)
16 threadNum = 1 #threadNum 指的是線程執行的批次。
17 threadPool = [] #線程池
18 while names:
19 for i in range(6):
20 try: #這裡需要考慮列表已經讀取完畢的情況
21 name = names.pop()
22 except IndexError as e:
23 print('The list is empty')
24 break
25 else:
26 t = threading.Thread(target=showName, args=(i, name, ))
27 threadPool.append(t)
28 t.start()
29 while threadPool: #也可以用for循環,然後清空threadPool線程池
30 t = threadPool.pop()
31 t.join() #使用join是為了阻塞主函數,意思是必須將t這個函數執行完畢後才能繼續執行主函數
32 threadNum += 1
33 print('--\r\n')
34 print('main is over ...')
以函數的形式使用多線程,就是用threading.Thread(target=functionName, args=(arguments,))的方法代入需要進行多線程操作的函數和函數所需的參數(如果沒有參數更好)。
如果代入的函數有一個參數,那麼args要寫成args=(arg1, )。如果有兩個參數,那麼args就要寫成args=(arg1, arg2, )。總之在參數元組的最後要留出一個空位。
在程序的第29~31行,使用了join函數是為了阻塞主函數,意思是主線程必須等待多線程執行完畢後才能正常結束。其實這裡也可以不用join函數,多線程的daemon屬性默認是false。這種情況下主線程本來就是要等待多線程執行完畢才會結束的。Join函數更多的是用在多進程。
time python3 threadingOfFunction.py圖2 函數式多線程
從圖2可以看出,這次操作只花費了4.002秒。與順序操作的20.045秒相比節約了一大半的時間。在計算機可以承受的範圍內,調大線程的數量,還可以將運行時間進一步縮短。
threadingOfClass.py的代碼如下:1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6 import threading
7
8 class ShowName(threading.Thread): #這裡的類名要大寫,該類繼承於threading.Thread類
9 def __init__(self, threadNum ,name):
10 threading.Thread.__init__(self) #這一步是必不可少的
11 self.name = name
12 self.threadNum = threadNum
13
14 def run(self):
15 nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))
16 print('I am thread-%d ,My name is function-%s, now time: %s '%(self.threadNum, self.name, nowTime))
17 time.sleep(1)
18
19 if __name__ == '__main__':
20 print('I am main ...')
21 names = [x for x in range(20)]
22 threadNum = 1
23 threadPool = []
24 while names:
25 for i in range(6):
6 try: #考慮線程已經讀取完畢的情況
27 name = names.pop()
28 except IndexError as e:
29 print('The List is empty')
30 break
31 else:
32 t = ShowName(i, name) #這裡調用ShowName幾乎與直接調用threading.Thread是一樣的。
33 threadPool.append(t)
34 t.start()
35 while threadPool:
36 t = threadPool.pop()
37 t.join()
38 threadNum += 1
39 print('===============\r\n')
40 print('main is over ...')
time python3 threadingOfClass.py圖3 類式多線程
從圖3中可以看到,threadingOfClass.py的運行時間是4.019秒,跟threadingOfFunction.py的4.002相當接近,說明這兩種方法從效率上來說沒有什麼區別,任選一種使用都可以。
這兩種方法都是對類的調用。所謂函數式的調用是對threading.Thread類的直接調用,而類式的調用則是先繼承threading.Thread類,重載子類的函數後再調用子類的方式調用,只是前者看起來更像函數調用而已。
多線程的好處在於可以並行運行重複性的工作,大大地減少了運行時間,但使用多線程也難免會忙中出錯。例如,重複地往一個文件內寫入單行內容。如果單線程的線性執行,很難出錯。如果多線程執行時那就不一定了。當多個線程同時向文件內寫入內容時,會不會造成一個線程寫入成功、其他的線程都做了無用功呢?不妨測試一下。
vi threadingWithoutLock.py1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6 import threading
7 import codecs
8
9 def showName(threadNum ,name):
10 with codecs.open('test.txt', 'a', 'utf-8') as fp:
11 nowTime = time.strftime('%H:%M:%S',time.localtime(time.time()))
12 fp.write('I am thread-%d ,My name is function-%s, now time:%s\r\n ' %(threadNum, name, nowTime))
13 print('I am thread-%d ,My name is function-%s, now time: %s '%(threadNum, name, nowTime))
14 time.sleep(1)
15
16
17 if __name__ == '__main__':
18 with codecs.open('test.txt', 'w', 'utf-8') as fp:
19 fp.write('')
20 print('I am main ...')
21 names = [x for x in range(100)]
22 threadNum = 1
23 threadPool = []
24 while names:
25 for i in range(13):
26 try:
27 name = names.pop()
28 except IndexError as e:
29 print('The list is empty')
30 break
31 else:
32 t = threading.Thread(target=showName, args=(i, name, ))
33 threadPool.append(t)
34 t.start()
35 while threadPool:
36 t = threadPool.pop()
37 t.join()
38 threadNum += 1
39 print('main is over ...')
這個程序與之前的多線程程序基本上沒什麼區別,只是增加了總共線程的數量(100個),並將輸出寫入了一個文件中。當執行的總線程比較少,同時執行的線程也不多的情況下也許不會出現問題。一旦數量上去了,問題就比較突出了。理論上names列表有100個元素,那麼就應該有100行字符串寫入到了test.txt文本中。
time python3 threadingWithoutLock.py圖4 Linux下未使用線程鎖的多線程
可以看出在Linux下還是正常的結果。現在將這個程序複製到Windows下執行,執行結果如圖5所示。
圖5 Windows下未使用線程鎖的多線程
只有91行的數據寫入到了文件內,還有9行數據丟失了。為了避免類似的情況,Python採用了線程鎖的方法確保文件的安全。使用線程鎖鎖定資源,避免幹擾。
在Python的多線程中有兩種鎖,一種是互斥鎖,另一種是可重入鎖。這兩者的區別是互斥鎖只能鎖定一次,解鎖一次,而可重入鎖可以鎖定多次。一般都是使用的互斥鎖。至於互斥鎖的效果如何,可以將上個例子稍微修改一下,加上互斥鎖測試一下即可。打開Putty連接到Linux,執行命令:
threadingWithLock.py的代碼如下:1 #!/usr/bin/env python3
2 #-*- coding:utf-8 -*-
3 __author__ = 'hstking hst_king@hotmail.com'
4
5 import time
6 import threading
7 import codecs
8
9 def showName(threadNum ,name):
10 mutex.acquire()
11 with codecs.open('test.txt', 'a', 'utf-8') as fp: #寫入到test.txt文件內
12 nowTime = time.strftime('%H:%M:%S', time.localtime(time.time()))
13 fp.write('I am thread-%d ,My name is function-%s, now time:%s\r\n ' %(threadNum, name, nowTime))
14 print('I am thread-%d ,My name is function-%s, now time: %s '
%(threadNum, name, nowTime))
15 mutex.release()
16 time.sleep(1)
17
18
19 if __name__ == '__main__':
20 with codecs.open('test.txt', 'w', 'utf-8') as fp:
21 fp.write('')
22 print('I am main ...')
23 mutex = threading.Lock()
24 names = [x for x in range(100)]
25 threadNum = 1
26 threadPool = []
27 while names:
28 for i in range(13):
29 try:
30 name = names.pop()
31 except IndexError as e:
32 print('The list is empty')
33 break
34 else:
35 t = threading.Thread(target=showName, args=(i, name, ))
36 threadPool.append(t)
37 t.start()
38 while threadPool:
39 t = threadPool.pop()
40 t.join()
41 threadNum += 1
42 print('main is over ...')
對比一下代碼,很容易理解,就是所有線程在寫入文件前加上一個互斥鎖,鎖定資源。等寫入完畢後再釋放互斥鎖。這樣就確保數據一定可以寫入到文件內了。執行程序測試一下。運行命令:
time python3 threadingWithLock.py圖6 Linux下使用線程鎖的多線程
檢查一下test.txt文件,正好100行,符合設計的結果。再將threadingWithLock.py程序複製到Windows下運行一下。執行結果如圖7所示。
圖7 Windows下使用線程鎖的多線程
通過對比可以看出使用線程鎖的情況下數據才能保證安全。程序才能按照設計的方式運行。如果不使用線程鎖,Linux下沒什麼問題(這只是特例,並不代表不使用線程鎖Linux下就是安全的)。Windows下必定會出現這樣那樣的問題。
微信公眾號:計算機與網絡安全
ID:Computer-network
【推薦書籍】