一 多道技術
1 技術背景
cpu在執行一個任務過程中,若需要操作硬盤的指令,指令一旦發出,硬盤上的機械手臂滑動讀取數據到內存中,這一段時間,cpu需要等待,時間可能很短,但對於cpu來說已經很長很長,長到可以讓cpu做很多其他的任務,如果我們讓cpu在這段時間內切到去做其他任務,這樣cpu不就充分利用了嗎?這正是多道技術產生的技術背景.
2 多道技術的含義
多到技術中的多道指的是多個程序,多道技術的實現是為了解決多個程序競爭或者說共享同一個資源(比如cpu)的有序調度問題,,解決方式即多路復用,多路復用分為時間上的復用和空間上的復用.
空間上的復用:將內存分為幾部分,每個部分放入一個程序,這樣,同一時間內存中就有了多道程序.
時間上的復用:當一個程序在等待I/O,另一個程序可以使用cpu,如果內存中可以同時存放足夠多的作業,則cpu的利用率可以接近100%.(操作系統采用了多道技術后,可以控制進程的切換,或者說進程之間去爭搶cpu的執行權限.這種切換不僅會在一個進程遇到io時進行,若一個進程占用cpu時間過程也會切換,或者說被操作系統奪走cpu的執行權限)
在A程序計算時,I/O空閑,A程序I/O操作時,CPU空閑(B程序也是同樣);必須A工作完成后,B 才能進入內存中開始工作,兩者是串行的,全部完成共需時間=T1+T2
空間上復用最大的問題是:程序之間的內存必須分割,這種分割需要在硬件層面實現,由操作系統控制,若內存彼此不分割,則一個程序可以訪問另一個程序的內存.
首先喪失的是安全性,比如qq程序可以訪問操作系統的內存,這意味着qq可以拿到操作系統的所有權限.
其次喪失的是穩定性,某個程序崩潰時可能把別的程序的內存也回收了,比方說把操作系統的內存給回收了,則操作系統崩潰
3 分時系統
由於CPU速度不斷提高和采用分時技術,一台計算機可同時連接多個用戶終端,而每個用戶可在自己的終端上聯機使用計算機,好像自己獨占機器一樣.
分時技術:把處理機的運行時間分成很短的時間片,按時間片輪流把處理機分配給各聯機作業使用.
若某個作業在分配給它的時間片內不能完成其計算,則該作業暫時中斷,把處理機讓給另一個作業使用,等待下一輪時再繼續其運行.由於計算機速度很快作業輪轉得很快,給每個用戶的印象是,好像他獨占了一台計算機.而每個用戶可以通過自己的終端向系統發出各種操作控制命令,在充分的人機交互情況下,完成作業的運行.
具有上述特征的計算機系統稱為分時系統,它允許多個用戶同時聯機使用計算機.
特點:
①多路性.若干個用戶同時使用一台計算機.微觀上看是各用戶輪流使用計算機;宏觀上看是各用戶並行工作.
②交互性.用戶可根據系統對請求的響應結果,進一步向系統提出新的請求.這種能使用戶與系統進行人機對話的工作方式,明顯地有別於批處理系統,因而,分時系統又被稱為交互式系統.
③獨立性.用戶之間可以相互獨立操作,互不干擾.系統保證各用戶程序運行的完整性.不會發生相互混淆或破壞現象.
④及時性.系統可對用戶的輸入及時作出響應.分時系統性能的主要指標之一是響應時間.它是指:從終端發出命令到系統予以應答所需的時間.
二 線程
1 什么是線程
在傳統操作系統中,每個進程有一個地址空間,而且默認就有一個控制線程.
線程顧名思義,就是一條流水線工作的過程,一條流水線必須屬於一個車間,一個車間的工作過程是一個進程
車間負責把資源整合到一起,是一個資源單位,而一個車間內至少有一個流水線,所以,進程只是用來把資源集中到一起(進程只是一個資源單位,或者說資源集合),而線程才是cpu上的執行單位.
多線程(即多個控制線程)的概念是,在一個進程中存在多個控制線程,多個控制線程共享該進程的地址空間,相當於一個車間內有多條流水線,都共用一個車間的資源.
Python3對多線程支持的是threading模塊,應用這個模塊,可以創建多線程程序,並且在多線程間進行同步和通信.在Python3中,可以通過以兩種方式來創建線程.即通過threading.Thread直接在線程中運行函數;通過繼承threading.Thread類來創建線程
2 為何要用多線程
多線程指的是,在一個進程中開啟多個線程,簡單的講:如果多個任務共用一塊地址空間,那么必須在一個進程內開啟多個線程.詳細的講分為4點:
①多線程共享一個進程的地址空間.
②線程比進程更輕量級,線程比進程更容易創建可撤銷,在許多操作系統中,創建一個線程比創建一個進程要快10-100倍,在有大量線程需要動態和快速修改時,這一特性很有用.
③若多個線程都是cpu密集型的,那么並不能獲得性能上的增強,但是如果存在大量的計算和大量I/O處理,擁有多個線程允許這些活動批次重疊運行,從而加快程序執行的速度.
④在多cou系統中,為了最大限度的利用多核,可以開啟多個線程,比開啟進程開銷小的多(這一條並不適用於python.詳見GIL)
3 開啟線程的兩種方式
①,使用threading.Thread直接在線程中運行函數
threading.Thread的基本使用方法如下:
Thread(group=None,target=None,name=None,args=(),kwargs={},*,demon=None)
其中target參數就是要運行的函數,args是傳入函數的參數元組.

#!/usr/bin/env python #-*coding:utf-8-*- import threading import time def sayhi(num): print('running on number:%s'%num) time.sleep(3) if __name__=='__main__': t1=threading.Thread(target=sayhi,args=(1,)) t2=threading.Thread(target=sayhi,args=(2,)) t1.start() t2.start()
②通過繼承threading.Thread類來創建線程
這種方法只要重載threading.Thread類的run方法,然后調用類的start()就能夠創建線程,並運行run()函數中的代碼;繼承threading.Thread()類中的子類中,如果需要重載__init__()方法,必須首先調用父類的__init__()方法,否則會引發AttributeError異常.

#!/usr/bin/env python #-*coding:utf-8-*- import threading import time class MyThread(threading.Thread): def __init__(self,num): super().__init__() self.num=num def run(self): print('running on number:%s'%self.num) time.sleep(3) if __name__=='__main__': t1=MyThread(1) t2=MyThread(2) t1.start() t2.start() print('ending......')
4 線程相關的方法
(1)threading.thread的實例方法
①join方法
作用:當某個線程或函數執行時需等待另一個線程完成操作后才能繼續,則應調用另一個線程的join()方法;其中的可選參數timeout用於指定線程運行的最長時間

#!/usr/bin/env python #-*coding:utf-8-*- import threading import time def thrfun(x,y,thr=None): if thr: thr.join() else: time.sleep(2) for i in range(x,y): print(str(i*i)+';') ta=threading.Thread(target=thrfun,args=(1,6)) tb=threading.Thread(target=thrfun,args=(16,21,ta)) ta.start() tb.start() ----------------------運行結果-------------------- D:\代碼\MyDjango\venv\Scripts\python.exe D:/代碼/MyDjango/Python基礎學習/進程和線程/thread1.py 1; 4; 9; 16; 25; 256; 289; 324; 361; 400; Process finished with exit code 0
②isAlive()方法
作用:用於查看線程是否運行
③daemon&setDaemon(True)
作用:daemon屬性和setDaemon(True)方法作用一樣,將線程聲明為守護線程,必須在start()方法調用之前設置.當我們在程序運行時,執行一個主線程,如果主線程又創建一個子線程,主線程和子線程就兵分兩路,分別運行,那么當主線程完成想退出時,會檢驗子線程是否完成.若子線程未完成,則主線程會等待子線程完成后再退出.但是有時候我們需要的是只要主線程完成了,不管子線程是否完成,都要和主線程一起退出,這時就可以用setDaemin方法或daemon屬性了,即用來設置線程是否隨主線程退出而退出,一般來說,其屬性值為True時會隨主線程退出而退出;

#!/usr/bin/env python #-*coding:utf-8-*- import threading import time class myThread(threading.Thread): def __init__(self,mynum): super().__init__() self.mynum=mynum def run(self): time.sleep(1) for i in range(self.mynum,self.mynum+5): print(str(i*i)+';') def main(): print('start...') ma=myThread(1) mb=myThread(16) #ma.daemon=True ma.setDaemon(True) #mb.daemon=True mb.setDaemon(True) ma.start() mb.start() print('end...') if __name__=='__main__': main()

from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.setDaemon(True) #必須在t.start()之前設置 t.start() print('主線程') print(t.is_alive()) ''' 主線程 True '''
注: 關於守護線程的注意點
①無論是進程還是線程,都遵循:守護XXX會等待主XXX運行完畢后被銷毀.
需要強調的是:運行完畢並非終止運行

#1.對主進程來說,運行完畢指的是主進程代碼運行完畢 #2.對主線程來說,運行完畢指的是主線程所在的進程內所有非守護線程統統運行完畢,主線程才算運行完畢 -------詳細解釋-------- #1 主進程在其代碼結束后就已經算運行完畢了(守護進程在此時就被回收),然后主進程會一直等非守護的子進程都運行完畢后回收子進程的資源(否則會產生僵屍進程),才會結束, #2 主線程在其他非守護線程運行完畢后才算運行完畢(守護線程在此時就被回收)。因為主線程的結束意味着進程的結束,進程整體的資源都將被回收,而進程必須保證非守護線程都運行完畢后才能結束。
(2) threading模塊提供的一些方法
threading.currentThread(): 返回當前的線程變量。
threading.enumerate(): 返回一個包含正在運行的線程的list。正在運行指線程啟動后、結束前,不包括啟動前和終止后的線程。
threading.activeCount(): 返回正在運行的線程數量,與len(threading.enumerate())有相同的結果
5 同步鎖(Lock)

import time import threading def addNum(): #在每個線程中都獲取這個全局變量 global num temp=num time.sleep(0.001) #對此公共變量進行-1操作 num=temp-1 num=100 #設定一個共享變量 thread_list=[] 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) --------------執行結果---------------- 預期結果為0,但實際不為0 原因:當一個子線程正在執行函數時,還沒有執行完畢cpu輪詢時間就到了,CPU的使用權強制交給了下一個子線程,造成共享數據破壞
解決方案:可使用同步鎖來解決上述問題
當一個進程擁有多個線程之后,如果它們各做各的任務沒有關系還行,可是既然同屬於一個進程,它們之間總是具有一定關系的.比如多個線程都要多某個數據進行修改,則可能會出現不可預料的結果.為了保證操作正確,就要對多個線程進行同步.
Python中可以使用threading模塊中的對象Lock和RLock進行簡單的線程同步.對於同一時刻只允許一個線程操作的數據對象,可以把操作過程放在Lock和RLock的acquire方法和release方法之間.RLock可以在同一調用鏈中多次請求而不會鎖死,Lock則會鎖死.(詳見下面的死鎖)
注:acquire和release這一對方法若前一個調用n次,后一個也要調用n次,鎖才能真正地釋放.

import time import threading R=threading.Lock() def addNum(): #在每個線程中都獲取這個全局變量 global num #加入鎖 R.acquire() temp=num time.sleep(0.001) #對此公共變量進行-1操作 num=temp-1 #釋放鎖 R.release() num=100 #設定一個共享變量 thread_list=[] 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)
6 死鎖現象與遞歸鎖
死鎖:是指兩個或兩個以上的進程或線程在執行過程中,因搶奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去.此時稱系統處於死鎖狀態或系統產生了死鎖,這樣永遠在互相等待的進程稱為死鎖進程,如下就是死鎖現象:

#!/usr/bin/env python #-*coding:utf-8-*- from threading import Thread,Lock import time mutexA=Lock() mutexB=Lock() class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A鎖\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B鎖\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B鎖\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A鎖\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start() ''' Thread-1 拿到A鎖 Thread-1 拿到B鎖 Thread-1 拿到B鎖 Thread-2 拿到A鎖 然后就卡住,死鎖了 '''
解決方案:遞歸鎖,在Python中為了支持在同一線程中多次請求同一資源,Python提供了可重入鎖RLock.
這個RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源可以被多次require.直到一個線程所有的acqiure都被release,其他的線程才能獲得資源.若使用RLock代替Lock,則不會發生死鎖:

#!/usr/bin/env python #-*coding:utf-8-*- import threading import time ''' 一個線程拿到鎖,counter加1,該線程內又碰到加鎖的情況,則counter繼續加1, 這期間所有其他線程都只能等待,等待該線程釋放所有鎖,即counter遞減到0為止 ''' mutexA=mutexB=threading.RLock() class MyThread(threading.Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A鎖\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B鎖\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B鎖\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A鎖\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start()
7 信號量 Semaphore
信號量用來控制線程並發數的,BoundedSemaphore或Semaphore管理一個內置的計數 器,每當調用acquire()時-1,調用release()時+1。
計數器不能小於0,當計數器為 0時,acquire()將阻塞線程至同步鎖定狀態,直到其他線程調用release()。(類似於停車位的概念)
BoundedSemaphore與Semaphore的唯一區別在於前者將在調用release()時檢查計數 器的值是否超過了計數器的初始值,如果超過了將拋出一個異常。

from threading import Thread,Semaphore import threading import time def func(sm): sm.acquire() print('%s get sm'%threading.current_thread().getName()) time.sleep(3) sm.release() if __name__=='__main__': sm=Semaphore(5) for i in range(23): t=Thread(target=func,args=(sm,)) t.start()
8 同步條件(Event)
線程的一個關鍵特性是每個線程都是獨立運行且狀態不可預測.如果程序中的其他線程需要通過判斷某個線程的狀態來確定自己下一步的操作,這時線程同步問題就會變得非常棘手.為了解決這些問題,我們需要使用threading庫中的Event對象.對象包含一個可由線程設置的信號標志,它允許線程等待某些事件的發生.在初始情況下,Event對象中的信號標志設置為假.若有線程等待一個Event對象,而這個Event對象的標志為假,那么這個線程會被一種阻塞直至該標志為真.一個線程如果將一個Event對象的信號標志設置為真,它將喚醒所有等待這個Event對象的線程.如果一個線程等待一個已經被設置為真的Event對象,那么它將忽略這個事件,繼續執行.
event.isSet():返回event的狀態值; event.wait():如果 event.isSet()==False將阻塞線程; event.set(): 設置event的狀態值為True,所有阻塞池的線程激活進入就緒狀態, 等待操作系統調度; event.clear():恢復event的狀態值為False。

import threading import time #演示了兩個線程通過Event來喚醒對方,模擬任務對話 evt=threading.Event() class myThreada(threading.Thread): def run(self): #print(self.name,'當前狀態:', evt.is_set()) evt.wait() print(self.name,':Good morning!') evt.clear() time.sleep(1) evt.set() time.sleep(1) evt.wait() print(self.name,"I'm fine,thank you.") class myThreadb(threading.Thread): def run(self): #print(self.name,'當前狀態:', evt.is_set()) print(self.name,':Good morning!') evt.set() time.sleep(1) evt.wait() print(self.name,'How are you?') evt.clear() time.sleep(1) evt.set() def main(): John=myThreada() John.name="John" Smith=myThreadb() Smith.name='Smith' John.start() Smith.start() if __name__=='__main__': main()
9 多線程利器---隊列(queue)

import threading,time li=[1,2,3,4,5] def pri(): while li: a=li[-1] print(a) time.sleep(1) try: li.remove(a) except Exception as e: print('----',a,e) t1=threading.Thread(target=pri,args=()) t1.start() t2=threading.Thread(target=pri,args=()) t2.start()

import queue ''' 隊列的長度可為無限或者有限.可通過Queue的構造函數的可選參數maxsize來設定隊列長度. 若maxsize小於1就表示隊列長度無限 ''' q=queue.Queue(maxsize=3) 將一個值放入隊列中 q.put(10) 調用隊列對象的put()方法在隊尾插入一個項目。put()有兩個參數,第一個item為必需的,為插入項目的值;第二個block為可選參數,默認為 1。如果隊列當前為空且block為1,put()方法就使調用線程暫停,直到空出一個數據單元。如果block為0,put方法將引發Full異常。 將一個值從隊列中取出 q.get() 調用隊列對象的get()方法從隊頭刪除並返回一個項目。可選參數為block,默認為True。如果隊列為空且block為True, get()就使調用線程暫停,直至有項目可用。如果隊列為空且block為False,隊列將引發Empty異常。 Python Queue模塊有三種隊列及構造函數: 1、Python Queue模塊的FIFO隊列先進先出。 class queue.Queue(maxsize) 2、LIFO類似於堆,即先進后出。 class queue.LifoQueue(maxsize) 3、還有一種是優先級隊列級別越低越先出來。 class queue.PriorityQueue(maxsize) 此包中的常用方法(q = Queue.Queue()): q.qsize() 返回隊列的大小 q.empty() 如果隊列為空,返回True,反之False q.full() 如果隊列滿了,返回True,反之False q.full 與 maxsize 大小對應 q.get([block[, timeout]]) 獲取隊列,timeout等待時間 q.get_nowait() 相當q.get(False) 非阻塞 q.put(item) 寫入隊列,timeout等待時間 q.put_nowait(item) 相當q.put(item, False) q.task_done() 在完成一項工作之后,q.task_done() 函數向任務已經完成的隊列發送一個信號 q.join() 實際上意味着等到隊列為空,再執行別的操作
10 GIL全局解釋鎖
<1>介紹
''' 定義: In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.) ''' 結論:在Cpython解釋器中,同一個進程下開啟的多線程,同一時刻只能有一個線程執行,無法利用多核優勢
首先需要明確一點是GIL並不是Python的特性,它是在實現Python解釋器(CPython)時所引入的一個概念.但是JPython就沒有GIL.然而因為CPython是大部分環境下默認的Python執行環境.所以在很多人的概念里CPython就是Python,也就想當然的把GIL歸結為Python語言的缺陷.所以這里要明確一點:GIL並不是Python的特性,Python完全可以不依賴GIL
<2>GIL介紹
GIL本質就是一把互斥鎖,既然是互斥鎖,所有互斥鎖的本質都一樣,都是將並發運行變成串行,以此來控制同一時間內共享數據只能被一個任務所修改,進而保證數據安全.
可以肯定的一點是:保護不同的數據安全,就應該加不同的鎖.
要想了解GIL,首先確定一點:每次執行python程序,都會產生一個獨立的進程.例如python aaa.py python bbb.py python ccc.py會產生3個不同的Python進程
在一個python的進程中,不僅有test.py的主線程或者由該主線程開啟的其他線程,還有解釋器開啟的垃圾回收等解釋器級別的線程,總之,所有線程都運行在這一個進程中
#1 所有數據都是共享的,這其中,代碼作為一種數據也是被所有線程共享的(test.py的所有代碼以及Cpython解釋器的所有代碼) 例如:test.py定義一個函數work(代碼內容如下圖),在進程內所有線程都能訪問到work的代碼,於是我們可以開啟三個線程然后target都指向該代碼,能訪問到意味着就是可以執行。 #2 所有線程的任務,都需要將任務的代碼當做參數傳給解釋器的代碼去執行,即所有的線程要想運行自己的任務,首先需要解決的是能夠訪問到解釋器的代碼。
綜上:
如果多個線程的target=work,那么執行流程是
多個線程先訪問到解釋器的代碼,即拿到執行權限,然后將target的代碼交給解釋器的代碼去執行
解釋器的代碼是所有線程共享的,所以垃圾回收線程也可能訪問到解釋器的代碼而去執行,這就導致了一個問題:對於同一個數據100,可能線程1執行x=100的同時,而垃圾回收執行的是回收100的操作,解決這種問題沒有什么高明的方法,就是加鎖處理,如下圖GIL,保證Python解釋器同一時間只能執行一個任務的代碼
<3>GIL與Lock
GIL保護的是解釋器級的數據,保護用戶自己的數據則需要自己加鎖處理,如下圖:
GIL與多線程的介紹:https://www.cnblogs.com/xiaoyuanqujing/protected/articles/11715730.html 密碼:xiaoyuanqujing@666
線程的其他知識點:https://www.cnblogs.com/xiaoyuanqujing/protected/articles/11715702.html