什麼是線程?
線程是操作系統能夠運算調度的最小單位。計算機中有各種各樣的應用程序,這些程序執行任何操作都需要CPU來計算,CPU計算需要操作系統發送指令給CPU,線程就相當於這一系列的指令 ,操作系統去調度CPU最小的單位就是線程。
什麼是進程?
進程是程序各種資源管理的集合。程序(線程)是不能單獨運行的,只有將程序裝載到內存中,系統爲其分配資源之後才能運行,而程序獲得各種資源後的集合就稱爲進程。例如:假如qq這個進程需要將數據發送到網絡,那麼就需要利用網卡,但是qq不能直接訪問網卡,所以此時就需要一個相當於中間介質的東西來管理多個進程之間的訪問,這個中間介質就是操作系統。 那麼qq就需要提供一個接口暴露給操作系統,以便被操作系統來管理。 這個接口裏麪包含對各種資源的調用,內存的管理,網絡接口的調用等(這個接口就稱之爲進程)。
進程和線程的關係:
線程被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。
例如:假如有一個工廠(這個工廠相當於整個內存空間),這個工廠有十個車間(每個車間相當於一個進程,每個進程佔用了部分內存空間,這個進程相當於這個車間內所有資源的集合),每個車間裏至少需要一個工人來工作(每個工人都相當於線程,同一個進程內的所有線程共享內存空間和資源),車間給工人提供相應的環境,而這些工人(線程)做實際的工作操作(發送數據和指令給cpu運算)。 車間沒有了工人就不能正常運行,而工人不在車間就不能完成某一項操作。
進程和線程之間的區別:
1、每一個進程中必須至少包含一個線程,進程是不執行的,執行的只有線程,操作一個進程實際上就是操作裏面的線程;
2、一個進行裏面可以包含多個線程,多個線程可以同時執行(通過上下文的快速切換進行實現,同一時刻只有一個線程在執行,通過不斷地切換程序使得看起來像是在並行),但同一個進程裏面的所有線程共用一個內存資源,所以修改一個線程,可能會有干擾其他線程;
3、進程與進程之間互不干擾,因爲不同進程內存空間不同,修改父進程是不會影響子進程,但刪除是會影響子進程;
4、創建一個新的線程比較容易,而創建一個新的進程需要從父進程處繼承;
5、進程與線程哪個速度快:其實這個問題是錯誤的,因爲進程和線程沒有可比性,進程是資源的集合,進程也是需要線程來發送指令給CPU計算的;
6、啓動一個進程快還是啓動一個線程快:啓進線程比進程快,因爲啓動進程需要到內存需申請空間和計算,啓動線程只是相當於發送指令。
併發:
一核CPU同一時刻只能處理一個任務,假如現在需要處理十個文檔,那麼就需要一個一個的來處理,需要處理十次。但是我們用十個任務分別處理這十個文檔就,對於CPU雖然不是同時的,但因爲CPU處理切換很快,給人感覺上就像是同時在處理。
Python threading模塊
threading模塊的兩種調用方式:
1 直接調用:
import threading, time
def run(i):
print("running threading:", i)
time.sleep(2)
print("threading %s running done..." % i)
t1 = threading.Thread(target=run, args=(1,)) # 實例化一個線程
t2 = threading.Thread(target=run, args=(2,)) # 實例化另一個線程
t1.start() # 啓動線程
t2.start()
# run(1)
# run(2)
運行結果:
running threading: 1
running threading: 2
threading 2 running done...
threading 1 running done...
# 直接運行run的時候可以看出,等待時間會更長,也就是4s,而啓動多線程時時間明顯縮短,下面會通過裝飾器直接測量多線程的運行時間
通過裝飾器來測量多線程的運行時間
import threading, time
# 設計一個裝飾器,來測量多線程的運行時間
def timeit(f):
def wrapper(*args, **kwargs):
start_time = time.time()
res = f(*args, **kwargs)
end_time = time.time()
print("%s函數運行時間爲:%.2f" %(f.__name__, end_time - start_time))
return res
return wrapper
def run(i):
print("running threading:", i)
time.sleep(2)
# print("threading %s running done..." % i)
@timeit
def main():
t1 = threading.Thread(target=run, args=(1,)) # 實例化一個線程
t2 = threading.Thread(target=run, args=(2,)) # 實例化另一個線程
t1.start() # 啓動線程
t2.start()
t1.join() # 等待線程執行完,才執行下一步
t2.join()
print("running done!")
main()
運行結果:
running threading: 1
running threading: 2
running done!
main函數運行時間爲:2.00
2 繼承式調用:
import threading
class MyThreading(threading.Thread):
def __init__(self, n):
super(MyThreading,self).__init__()
self.n = n
def run(self): # 這種方式下必須將子函數寫成run
print("run task:",self.n)
t1 = MyThreading("1")
t2 = MyThreading("2")
t1.start()
t2.start()
守護線程(Daemon):
正常情況下主線程和子線程是併發的,如果整個程序的線程沒有執行完,那麼就不會退出該程序,必須等全部執行完纔會退出(在不適用.join的情況下)。如果將子進程被設置爲守護線程,當主線程執行完成後,不管子線程有沒有執行完,整個程序都會退出。
import threading
import time
def run(n):
print ("task",n)
time.sleep(2)
print ("task done",n)
start_time = time.time()
t_objs = [] # 建立一個空列表,用於存放實例化後的線程t
for i in range(50):
t = threading.Thread(target=run,args=("t-%s"%i,))
t.setDaemon(True) ## 把當前線程設置爲守護線程;設置守護之前一定要在t.start()之前,在t.start()之後就不允許設置了。
t.start()
t_objs.append(t)
# for t in t_objs: #這裏需要把.join的操作註釋掉
# t.join()
print ("finished!")
print ("running time:",time.time() - start_time)
運行結果:
task t-0
task t-1
task t-2
....
task t-48
task t-49
finished!
running time: 0.00800013542175293
Process finished with exit code 0
可以看出時間很短,因爲主線程已經執行完了,守護線程不管有沒有運行完,程序都退出了
全局鎖GIL(Global Interpreter Lock):
python有通過C語言開發的,也有通過JAVA開發的。C語言開發的就叫做cpython,Java開發的就叫做jpython。其中cpython存在一個問題需要使用到全局鎖。
此時假如有4個任務,如果只有一核CPU,那麼執行任務時,只能是串行的方式執行,不是並行的方式,執行任務1時,如果任務1沒有計算完成,可能會切換到任務2去計算,那麼此時任務1會被記住已經執行到了哪一頁,哪一行,哪一個具體字符的位置(稱作:上下文),等到下次切換到任務1時,就繼續之前的位置計算執行。
如果有4核CPU,就可以同時執行4個任務,實現真正的在同一時刻併發,其中Java和c都可以實現真正的併發;而python是假併發,這是因爲當初python在設計時還沒有多核CPU,所以就沒考慮使用多核CPU來實現併發,而導致了python設計缺陷; 當前python只是因爲CPU計算切換的較快,所以看到的是假併發的錯覺。python不管計算機有多少核CPU,在同一時刻,能計算的線程只有一個。
假如現在有個數據 number = 1,此時我們起4個線程,希望每個線程+1後,根據加減結果另外線程再去+1,我們同時交給4核CPU分別取處理去+1(要求最終結果等於4),線程1將數據交給了CPU1,線程2將數據交給了CPU2......,每核CPU在同一時間獲取了number = 1這個數據,然後分別各自去做 +1 這個操作等於2,而我們每核CPU會將2這個結果返還給number = 2,所以最後number還是 =2,並不是我們期望的4核CPU分別+1最終=4。像加減運算這類的數據,我們還是期望使用串行的方式,一個一個的去計算,而不是併發導致最終計算錯誤。此時我們就可以使用全局鎖來解決cpython中出現的這個問題。
雖然4個線程將數據同時交給了4個CPU,但線程到python解釋器去申請全局鎖後才能執行(python解釋器負責全局鎖分配),得到gil鎖後就正常執行沒得到gil鎖的就不能執行,同一時間只能有一個線程獲取gil鎖,這樣可以避免所有線程同時去計算。 但gil鎖也有個問題是,其中一個線程拿到gil鎖後會對線程有執行的時間限制。假如線程1拿到了gil鎖,一共需要執行5秒才能完成線程1的代碼,但是gil鎖只給了線程1秒的執行時間就必須釋放gil鎖給其他線程輪詢執行(此時CPU會記住線程1的位置,也就是上下文),此時線程1還沒執行完成,線程2就拿到gil鎖去執行可能就會導致最終共享數據計算錯誤。在python2.x的版本中會出現上述的問題,而在python3.x中就不再出現,可能是自動加了鎖。下圖爲GIL的基本實現過程:
下面我們用0+1來計算模擬問題:
步驟1:線程1拿到共享數據
步驟2:線程1到python解釋器去申請gil鎖。
步驟3:解釋器調用系統原生線程(操作系統的線程)
步驟4:將線程1的任務分配到CPU1。
步驟5:此時線程1還沒有執行完成,因爲執行時間到了,被要求釋放gil鎖。
步驟6:線程2拿到共享數據。
步驟7:線程2到解釋器去申請gil鎖。
步驟8:調用原生線程。
步驟9:線程2被分配到了CPU3.
步驟10:此時由於線程2執行較快,在執行時間內完成了計算,返回給解釋器。
步驟11:count默認=0,此時線程2進行0+1=1,將1這個值賦值給了count,count此時=1。
步驟12:線程1重複第一次執行的所有動作。
步驟13:此時線程1也計算完成,將0+1 ,count=1的結果賦值給count,此時會覆蓋count的數據,所以最終count還是=1,。
線程鎖(互斥鎖Mutex):
有全局鎖,依然會存在修改共享數據的問題,由於一個進程下可以啓動多個線程,多個線程共享父進程的內存空間,也就意味着每個線程可以訪問同一份數據,此時,如果多個線程同時要修改同一份數據,只有全局鎖的話,最終的數據可能還是會出現問題。,那麼我們可以通過再加一把鎖來解決這個問題。再加的鎖就不是全局鎖了(而是線程鎖),和全局鎖也沒有關係,而是針對數據加一把鎖,新加鎖後只能其中一個線程來修改這個數據。 也就是說計算的時候依然使用全局鎖,但是改變數據時只能等待拿到新鎖的線程修改完後,其他線程才能去修改這個數據。假如線程1拿到了修改數據的新鎖,此時線程2計算的比線程1要快,但線程2不能改變數據,只能等線程1修改完後,線程2才能去獲取鎖在修改。最終多線程還是可以實現的,只是計算的過程實現了並行,而修改結果變成了串行。
import time
import threading
def addNum():
global num #在每個線程中都獲取這個全局變量
print('Get num:',num )
time.sleep(1)
lock.acquire() #修改數據前加鎖
num -=1 #對此公共變量進行-1操作
lock.release() #修改後釋放
num = 100 #設定一個共享變量
thread_list = []
lock = threading.Lock() #生成全局鎖
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: #等待所有線程執行完畢
t.join()
print('Final num:', num )
遞歸鎖(RLock):
可以理解爲在鎖中再嵌套子鎖
import threading, time
def run1():
print("grab the first part data")
lock.acquire()
global num
num += 1
lock.release()
return num
def run2():
print("grab the second part data")
lock.acquire()
global num2
num2 += 1
lock.release()
return num2
def run3():
lock.acquire()
res = run1()
print('--------between run1 and run2-----')
res2 = run2()
lock.release()
print(res, res2)
if __name__ == '__main__':
num, num2 = 0, 0
lock = threading.RLock()
for i in range(2):
t = threading.Thread(target=run3)
t.start()
# while threading.active_count() != 1:
# print(threading.active_count())
# else:
# print('----all threads done---')
# print(num, num2)
運行結果;
grab the first part data
--------between run1 and run2-----
grab the second part data
1 1
grab the first part data
--------between run1 and run2-----
grab the second part data
2 2
信息量(Semaphore):
互斥鎖是指同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據 ,比如廁所有3個坑,那最多隻允許3個人上廁所,後面的人只能等裏面有人出來了才能再進去。
import threading, time
def run(n):
semaphore.acquire()
time.sleep(1)
print("run the thread: %s" % n)
semaphore.release()
if __name__ == '__main__':
num = 0
semaphore = threading.BoundedSemaphore(10) # 最多允許10個線程同時運行
for i in range(20):
t = threading.Thread(target=run, args=(i,))
t.start()
# 運行代碼就可以看到結果是10個10個一出
事件(Event):
事件用於線程與線程之間的交互,主要有三種用法:
- event.wait()等待標誌被設置,如果標誌被設置了,wait方法什麼都不做,直接往運行,否則等待被設置。
- event.clear() 清空標誌
- event.set() 設置標誌
import threading
def do(event):
print("start")
event.wait()
print("execute")
event_obj = threading.Event()
for i in range(3):
t = threading.Thread(target=do, args=(event_obj,))
t.start()
event_obj.clear() # 清空標誌符
inp = input('input:')
if inp == 'true':
event_obj.set() # 設置標誌符
運行結果:
start
start
start
input:true
execute
execute
execute
隊列(queue):
當要在多線程之間實現數據的安全交互時,隊列是一種很好地解決方式。隊列其實是一種解耦的過程,從而降低了程序之間的關聯性。有三種方式:
class queue.
Queue
(maxsize=0) #先入先出
class queue.
LifoQueue
(maxsize=0) #last in fisrt out
class queue.
PriorityQueue
(maxsize=0) #存儲數據時可設置優先級的隊列
import queue
# q = queue.Queue() # 先進先出
# # q = queue.LifoQueue() # 後進先出
#
# q.put(1)
# q.put(2)
# q.put(3)
# print(q.get())
# print(q.get())
# print(q.get())
q = queue.PriorityQueue() # 設置優先級
q.put(1,"lkj") # 1表示優先級,數字越小優先級越高
q.put(10,"dak")
q.put(3,"sjdlaj")
print(q.get())
print(q.get())
print(q.get())
詳細可參考:
https://blog.51cto.com/daimalaobing/2087351
https://www.cnblogs.com/alex3714/articles/5230609.html