線程(上) 1.線程含義:一段指令集,也就是一個執行某個程式的代碼。不管你執行的是什麼,代碼量少與多,都會重新翻譯為一段指令集。可以理解為輕量級進程 比如,ipconfig,或者, python XX.py(執行某個py程式),這些都是指令集和,也就是各自都是一個線程。 2.線程的特性: 線程之間可 ...
線程(上)
1.線程含義:一段指令集,也就是一個執行某個程式的代碼。不管你執行的是什麼,代碼量少與多,都會重新翻譯為一段指令集。可以理解為輕量級進程
比如,ipconfig,或者, python XX.py(執行某個py程式),這些都是指令集和,也就是各自都是一個線程。
2.線程的特性:
-
線程之間可以相互通信,數據共用
-
線程並不等同於進程
-
線程有一定局限性
-
線程的速度由CPU和GIL決定。
GIL,GIL全稱Global Interpreter Lock,全局解釋鎖,此處暫且不談,再下麵該出現的地方會做仔細的講解。
3.python中的線程由內置模塊Threading整合
例1:簡答的線程應用:
我們先看看這段代碼
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
begin = time.time()
def func1():
time.sleep(2)
print(func1.__name__)
def func2():
time.sleep(2)
print(func2.__name__)
func1()
func2()
end = time.time()
print(end-begin)
結果:
用時差不多4s對吧。好的,當我們使用線程來修改這段代碼
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
begin = time.time()
def func1():
time.sleep(2)
print(func1.__name__)
def func2():
time.sleep(2)
print(func2.__name__)
'''創建線程對象,target參數為函數名,args可以為列表或元組,列表/元組
內的參數即為函數的參數,這裡兩個函數本就沒有參數,所以設定為空,'''
t1 = threading.Thread(target=func1,args=[])
t2 = threading.Thread(target=func2,args=[])
#開始進程
t1.start()
t2.start()
end = time.time()
print(end-begin)
運行結果:
卧槽?啥情況?咋成了0s。這裡要註意了,這裡的是時間先出來,函數的列印語句後出來,那麼就表示整個程式里的兩個線程是同時進行的,並且沒有等線程運行結束就運行到下麵的列印用時語句了。註意這裡的幾個字“沒有等線程運行結束”。所以這裡就有問題對吧?沒關係的,線程給我們準備了一個方法——join,join方法的用意就是等線程運行結束再執行後面的代碼,那麼我們加上join再看
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
begin = time.time()
def func1():
time.sleep(2)
print(func1.__name__)
def func2():
time.sleep(2)
print(func2.__name__)
'''創建線程對象,target參數為函數名,args可以為列表或元組,列表/元組
內的參數即為函數的參數,這裡兩個函數本就沒有參數,所以設定為空,'''
t1 = threading.Thread(target=func1,args=[])
t2 = threading.Thread(target=func2,args=[])
#開始進程
t1.start()
t2.start()
#等待線程運行結束
t1.join()
t2.join()
end = time.time()
print(end-begin)
看看結果呢?
正常了對吧?時間最後出現,並且和沒使用線程時節省了整整一倍對吧,那麼按照常理我們都會認為這兩個線程是同時運行的對吧?那麼真的是這樣嗎?
因為都知道一個常識,一個CPU只能同時處理一件事(這裡暫且設定這個CPU是單核),而這整個程式其實就是一個主線程,此處的主線程包括了有兩個線程。這整個下來,程式運行的每個步驟是這樣的:
第一步:先運行func1,因為線程t1在前面。
第二步:運行到睡眠語句時,因為睡眠語句時不占CPU,所以立馬切換到func2
第三部:運行func2
第四步:運行到睡眠語句,立馬又切換到func1的列印語句
第五部:func1整個運行完,立馬切換到func2的列印語句,結束整個程式
所以你看似是同時,其實並不是同時運行,只是誰沒有占用CPU就會立馬把運行權利放開給其他線程運行,這樣交叉運行下來就完成了整個程式的運行。就這麼簡單,沒什麼難度對吧?
此時我設定的函數是不帶參數,當然你可以試試帶參數,效果也是一樣的
再說明一下join的特性,join的字面意思就是加入某個組織,線程里的join意思就是加入隊列。
就好比去票站排隊買票一樣,前面的人完了才到你,票站開設一天為排好隊的人售票,那麼這裡的票站就是一個主線程,隊伍中的每個人各自都是一個線程,不過這個買票站不止有一個視窗,當前面的正在買票的人耗費很多時間時,那麼後面排隊的人如果看到其他的視窗人少就會重新排到新的隊伍中以此來節省排隊時間,儘快買到票,直到票站里的工作人員下班結束售票(整個進程結束)。我這麼說的話,相信很多人就懂了吧?生活常識對吧?
而這裡的兩個線程(或者你可以給三個、四個以上)結合起來就叫多線程(並不是真正意義上的,看後面可得),此時的兩個線程並不是同時進行,也不是串列(即一個一個來),而是併發的
例2:對比python2和python3中線程的不同
先看python3下的:
不使用線程:
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
begin = time.time()
def func(n):
res = 0
for i in range(n):
res += i
print('結果為:',res)
func(10000000)
func(20000000)
end = time.time()
print(end-begin)
運行結果:
使用線程:
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
begin = time.time()
def func(n):
res = 0
for i in range(n):
res += i
print('結果為:',res)
t1 = threading.Thread(target=func,args=(10000000,))
t2 = threading.Thread(target=func,args=(20000000,))
#開始進程
t1.start()
t2.start()
#等待線程運行結束
t1.join()
t2.join()
end = time.time()
print(end-begin)
運行結果:
差距竟然很小了對吧?和前面使用sleep的結果完全不一樣了。
再看python2下:
不使用線程:
代碼和前面的一樣,不浪費時間了,運行結果:
使用線程:
發現居然還比不使用線程還慢,卧槽,那我還搞毛的線程啊。不急著說這個
從python2和python3的對比下,相信你已經知道了,python3優化的很不錯了,基本能和不使用線程鎖耗時間一樣。並且同樣的代碼,不使用線程下的版本2和版本3的對比都感覺時間縮短了,這就是python3的優化。
那麼這種為何不能和前面的sleep運行的結果成倍的減少呢?在版本2里反而還不減反增。這一種就是計算密集型線程。而前面的例子使用time模塊的就是IO密集型線程
IO密集型:IO占用的操作,前面的time.sleep的操作和文件IO占用的則為IO密集型
計算密集型:通過計算的類型
好的,開始說說這個使用線程為何還是沒有很明顯節省資源了,前面我提到的,一個CPU只能同時處理一件事(這裡暫且設定這個CPU是單核),關鍵就在於CPU是單核,但相信大家對自己的電腦都很瞭解,比如我的電腦是四核的,還有的朋友的CPU可能是雙核,但再怎麼也不可能是單核對吧?單核CPU的時代已經過去了。
但是這裡它就是一個BUG,究其根源也就是前面提到的GIL,全局解釋鎖
4.全局解釋鎖GIL
1)含義:
GIL,全局解釋鎖,由解釋器決定有無。常規里我們使用的是Cpython,python調用的底層指令就是藉助C語言來實現的,即在C語言基礎上的python,還有Jpython等等的,而只有Cpython才有這個GIL,而這個GIL並不是Python的特性,也就是這個問題並不是python自身的問題,而是這個C下的解釋器問題。
在Cpython下的運行流程就是這樣的
由於有這個GIL,所以在同一時刻只能有一個線程進入解釋器。
龜數在開發Cpython時,就已經有這個GIL了,當他開發時,由於有可能會有一些數據操作風險,比如同時又兩個線程拿一個數據,那麼操作後就會有不可預估的後患了,而龜數當時為了避免這個問題,而當時也正是CPU單核時期,所以直接就加了這個GIL,防止同一時刻多個線程去操作同一個數據。
那麼到了多核CPU時代,這個解決辦法在現在來看就是一個BUG了。
總之,python到目前為止,沒有真正意義上的多線程,不能同時有多個線程操作一個數據,並且這個GIL也已經去不掉了,很早就有人為了取消GIL而奮鬥著,但是還是失敗了,反正Cpython下,就是有這麼個問題,在python3中只是相對的優化了,也沒有根本的解決GIL。並且只在計算密集型里體現的很明顯
那麼有朋友覺得,卧槽,好XX坑啊,那我XX還學個啥玩意兒啊,崩潰中,哈哈哈
沒法啊,就是這麼個現狀,但是多線程既然開不了,可以開多進程和協程啊。而且在以後還是有很多替代方案的。
總結:
根據需求選擇方案。
如果是IO密集型:使用線程
如果是計算密集型:使用多進程/C語言指令/協程
5.setDaemon特性
好的,來點實際的
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
begin = time.time()
def music(name):
for i in range(2):
print('I listenning the music %s,%s'%(name,time.ctime()))
time.sleep(2)
print('end listenning %s'%time.ctime())
def movie(name):
for i in range(2):
print('I am watching the movie %s,%s'%(name,time.ctime()))
time.sleep(3)
print('end wachting %s'%time.ctime())
t1 = threading.Thread(target=music,args = ('晴天-周傑倫',) )
t2 = threading.Thread(target=movie,args=('霸王別姬',))
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(end - begin)
查看運行結果:
因為這是IO密集型的,所以可以有多線程的效果。
那麼在很多的開發中,還有另一種寫法
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
begin = time.time()
def music(name):
for i in range(2):
print('I listenning the music %s,%s'%(name,time.ctime()))
time.sleep(2)
print('end listenning %s'%time.ctime())
def movie(name):
for i in range(2):
print('I am watching the movie %s,%s'%(name,time.ctime()))
time.sleep(3)
print('end wachting %s'%time.ctime())
threads = []
t1 = threading.Thread(target=music,args = ('晴天-周傑倫',) )
t2 = threading.Thread(target=movie,args=('霸王別姬',))
threads.append(t1)
threads.append(t2)
for i in threads:
i.start()
i.join()
end = time.time()
print(end - begin)
而這種寫法的運行結果:
咋回事,10s,註意了,這是很多人容易犯的錯
首先要說下,join是等程式執行完再往下走,所以join帶有阻塞功能,當你把i.join()放到for迴圈裡面, 那麼聽音樂的線程必須結束後再執行看電影的線程,也就是整個程式變成串列了對吧?
所以正確的寫法是這樣:
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
begin = time.time()
def music(name):
for i in range(2):
print('I listenning the music %s,%s'%(name,time.ctime()))
time.sleep(2)
print('end listenning %s'%time.ctime())
def movie(name):
for i in range(2):
print('I am watching the movie %s,%s'%(name,time.ctime()))
time.sleep(3)
print('end wachting %s'%time.ctime())
threads = []
t1 = threading.Thread(target=music,args = ('晴天-周傑倫',) )
t2 = threading.Thread(target=movie,args=('霸王別姬',))
threads.append(t1)
threads.append(t2)
for i in threads:
i.start()
i.join()
end = time.time()
print(end - begin)
運行結果:
結果和前面的寫法一樣了對吧?說下,for迴圈下的i,我們可以知道i一定是for結束後的最後的值,不信的話可以試試這個簡單的:
那麼說回上面的問題,當i.join()時,此時的i一定是t2對不對?那麼整個程式就在t2阻塞住了,直到t2執行完了才執行列印總用時語句,既然執行t2,因為執行t2要6秒,而t1要4秒,那麼可以確定,在t2執行完時,t1絕對執行完了的。或者換個說法,for迴圈開始,t1和t2誰先開始不一定,因為線程都是搶著執行,但一定是t1先結束,然後再是t2結束,再結束整個程式。所以說,只有把i.join()放在for迴圈外,才真的達到了多線程的效果。
好的,再說一個有趣的東西,不多說,直接看
未截到圖的區域和上面的一樣,不浪費時間了。看到了嗎?最後列印的時間居然在第三排,如果你們自己測試了的話,就知道這列印時間語句和上面兩個是同時出現的,咋回事,因為這是主線程啊,主線程和兩個子線程同時運行的,所以這樣,那麼我們加一個東西
加了一個setDaemon(True),這個方法的意思是設置守護進程,並且要註意,這個必須在設置的線程start()方法之前
咦?主線程運行後就直接結束了,這啥情況呢?那再設置在子線程上呢:
設置在t1(聽音樂)上:
再設置在t2(看電影)上:
看出什麼問題了嗎?
好的,不廢話,直接說作用吧,setDaemon是守護進程的意思,而這裡我們用線上程上,也就是對線程的守護。設置誰做為守護線程(進程),那麼當此線程結束後就不管被守護的線程(進程)結束與否,程式是否結束全在於其他線程運行結束與否,但被守護的線程也一直正常的在運行。所以上面的主線程設置守護線程後,因為等不到其他同級別的線程運行所以就直接結束了。而當設置t1作為守護線程時,程式就不管t1了,開始在意其他線程t2運行結束與否,但同時還是在運行自己,因為t2運行時間比t1久,所以t1和t2還是正常的運行了。而當設置t2作為守護線程時,當t1聽完音樂結束,整個程式也結束了,而t2並沒有正常的結束,不過一直存在的,就是這麼個意思
6.通過自定義類設置線程
#!usr/bin/env python
#-*- coding:utf-8 -*-
# author:yangva
import threading,time
class mythread(threading.Thread):
def __init__(self,name):
super(mythread,self).__init__()
self.name = name
def run(self): #對繼承threading的重寫方法
print('%s is rurning'%self.name)
time.sleep(2)
t = mythread('yang')
t.start()
運行結果:
沒啥特點對不對,其實就是寫了一個類繼承thread,然後運行而已。本質上以上的代碼和下麵這一段沒區別:
好的,本篇博文暫且到這裡,還沒完,下一篇的才是重頭戲