多任務
- Linux是多用戶多任務的操作系統
- 多任務的意思就是同時進行。比如,不能先站着唱完歌,再跳舞。
- 怎麼同時進行呢?
答:cpu的多個核同時工作。
- 但是這就有一個問題了,現在電腦普遍四核,高端點八核。最多隻能同時處理八個任務?還有我手上現在用來編程的單片機,只有一核,只能處理一個任務?
- 顯然不是的。當任務數小於核心數,就是真的多任務,叫並行
- 當任務數大於核心數,就是假的多任務,叫併發
- 併發 就是某個核執行任務時,快速的切換,讓我們感覺好像在一起運行一樣。可能每個程序執行0.0000…1秒就切換到下一個程序了。換言之,就是切換任務的速度非常快,使我們產生了錯覺。
- CPU一秒鐘執行好幾百萬次。
- 具體每一個程序分配多久,取決於算法調度(比如優先級調度中,因爲聽歌之類的不希望有斷斷續續的感覺,所以聽歌的優先級很高)。
一、多線程
- 程序是按照結構一行一行執行的,這被稱爲一個線程。一個程序運行起來之後,一定有一個執行代碼的東西,這個東西就被稱之爲線程。
- 想像一個箭頭,它指到哪就執行到哪。這個箭頭就是一個線程,多線程就是有多個箭頭。
1)對比單線程與多線程
1.單線程
- 代碼如下:
- 效果如下(耗時6秒):
2.多線程
- threading模塊中有一個類叫做Thread,給它的Target參數傳入方法/函數/和一切可執行的操作創建出來一個實例。只要這個實例調用了start方法,就生成一個獨立的線程。
import threading.Thread
......
變量名 = Thread(target=函數名)
變量名 = Thread(target=函數名)
變量名 = Thread(target=函數名)
- 代碼如下:
- 效果如下(耗時1.5秒):
2)enumerate()方法
- 一個程序啓動時,一定有一個線程叫做主線程。當調用模塊,開啓另一個線程,這個線程就叫子線程。
- 主線程沒有可執行的代碼時,子線程若還在,主線程會等子線程結束。
- enumerate()可以查看程序當前的線程。
len(threading.enumerate())
- enumerate函數,可以將可迭代數據的每一個元素,變爲元組。當然,threading中的這個函數不是這個意思哈,只是示例。
- 看看把延時模塊刪除之後發生了什麼,代碼如下(導入模塊代碼變化,新增線程數量代碼):
- 執行效果如下(重複執行三次,看看有何不同):
- 會發現所有線程執行沒有先後順序。說明線程的執行是看操作系統調度的(看它心情)。
- 想指定線程的先後順序,可以使用延時
- 如果想所有子線程結束後程序就結束,可以用if加enumerate長度判斷退出。
- 主線程如果先結束了,子線程必結束。
- 子線程是從調用start()開始的,可以用enumerate長度判斷
3)多線程執行類
- 要通過多線程執行類,需要繼承threading.Thread,調用start()時,start()會自動調用類裏面的run方法。注意一個start()值生成一個線程
Class NewClass(threading.Thread):
def 函數名(self):
......
if __name__ == "__main__":
變量名 = 類名()
變量名.start()
- 代碼如下:
- 結果如下:
這裏沒有使用延時,說明只有一個子線程。(主線程在等子線程執行完畢)
4)子線程之間使用的全局變量可以共享
- 使用一個函數修改全局變量,看看另一個函數調用的結果如何
- 代碼如下:
- 結果如下:
- 說明全局變量可共享
5)通過args爲函數傳遞參數
- 直接一個函數名是使用變量
- 函數名加括號是調用函數
可是沒有括號如何爲函數傳遞參數呢?
threading.Thread中有一個參數可爲函數傳遞參數,就是args,它接收的參數是一個元組。
- 代碼:
- 效果:
- 多任務往往配合使用,所以要共享
6)資源競爭
- 多線程有可能會出現資源競爭,導致報錯
- 代碼如下,把times改爲一百萬看看
- times等於100時:
- times等於100萬時:
- 怎麼不等於200萬?python是一門高度簡潔的語言,gl_num += 1,其實可以分爲許多步
- 找到變量
- 更改變量引用
- 存儲變量引用
- 然後CPU在分配任務時,可能變量改變還沒存儲,就切到另一個線程去了。
- 那爲什麼100不會出錯呢?答:會出錯。出錯是個概率,樣本數太小了,恰好沒錯而已。
7)解決資源競爭
- 之所以出問題,就是因爲一個線程還沒走完,就去執行另一個線程了。可以通過線程同步來解決。
- 爲了不發生資源競爭,就要讓一個線程走完,這就是原子性,在這裏也可以叫事務。
- 同步就是協同步調,按預定的先後次序進行運行
1.互斥鎖
- 當多個線程幾乎同時修改一個共享數據時,就需要進行同步控制
- 某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。
# 創建互斥鎖,默認是沒有上鎖的
mutex = threading.Lock()
# 上鎖(取得)
mutex.acquire()
# 解鎖(釋放)
mutex.release()
- 同一把鎖,只能上一次,誰先上誰用。
2.上鎖使一個線程執行完
- 代碼:
- 結果:
3.上鎖使一個線程必要的部分執行完
- 執行完一個線程,再執行另一個線程,和單線程有啥區別?
- 所以,上的鎖越小越好。
- 代碼:
- 結果:
8)死鎖
- 死鎖是一種狀態
- 在線程間共享多個資源的時候,如果兩個線程分別佔有一部分資源並且同時等待對方的資源,就會造成死鎖。
避免死鎖
- 程序設計時要儘量避免(銀行家算法)
- 添加超時時間
銀行裏有10億時,憑什麼敢放貸20億?得益於銀行家算法。
→銀行家算法←
- 在設計時,心裏要有一杆秤,到上面時候哪一把鎖會解開,解開後會導致連環解鎖。
9)udp多線程聊天器
- 有一個問題:是該創建一個套接字呢,還是兩個?答案是一個,爲什麼?因爲udp是雙工,既可以接又可以發。
- 把接收和發送的功能封裝。然後添加線程即可,代碼如下:
- 效果圖就不貼了。
二、多進程
- 進程就是進行中的程序。進程擁有資源(比如網卡信息,攝像頭使用能力等),進程就是一個資源分配的代碼塊。
- 進程 = 代碼 + 用到的資源
- 進程有三個狀態:就緒態、執行態、等待態
1)實現多進程
- 和多線程一模一樣(就換個對象。我把多線程改成多進程,都是直接使用末行模式的替換):
- shell中,輸入 ps可以看到正在運行的進程(只有部分,想看到全部加-aux):
這裏的三個就是一個主進程,兩個子進程
殺死進程,shell中用kill 進程ID
2)進程補充
- 多進程是什麼?
- 進程就是代碼加資源。多進程就是,把資源複製一份,然後指定了代碼的開始位置。(代碼不復制是因爲,代碼不會變,是共享的)
- 進程耗費的資源大
- 進程浪費了內存,但是提高了效率(一定範圍內的,如果進程數過多,會卡)
- 原則上,能共享就共享。實在當,通過特殊手段修改代碼時,代碼纔會複製。這叫寫時拷貝
進程與線程的區別
- 先有進程,纔有線程。
- 進程僅僅是一個資源分配的單位,資源單位的總和,上面的線程拿的資源最多。
- 線程是進程調度和分配的基本單位。
- 多線程就是同一個資源裏有多個執行代碼的東西。
- 多進程就是有多個資源。
- 區別:
- 線程的劃分尺度小於進程(資源比進程少),使得多線程程序的併發性高。
- 一個程序至少有一個進程,一個進程至少有一個線程.
- 進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率
- 線程和進程在使用上各有優缺點:線程執行開銷小,但不利於資源的管理和保護;而進程正相反。
3)進程間通信
- 線程間資源是共享的,美滋滋。但是進程間不是,需要用對方的資源,必須通過通信。
- 進程間通信,有很多種機制:
- 進程間通信,可以用socket。
- 進程間通信,可以用文件(文件在硬盤上,所以速度較慢)。
- 可以設想,如果在內存中開闢一塊地方,進程把數據存到這個內存中,另一個進程訪問這塊內存就完成了通信。
- Queue隊列
- 先進先出,就叫隊列
- 用Queue的一個主要目的是:解耦
耦合性高,有可能,改了一小塊代碼,其它一大片地方要跟着改,不然程序就完蛋了。
- 隊列使用的代碼:
- 需要在創建進程前就創建好一個隊列。
- 實際操作時代碼如下:
- 效果如下
4)進程池Pool
- 進程池,就是先創建一個池,裏面有許多進程,先執行一部分進程,然後某些進程結束了,新的進程通過重複利用,來提高效率,節省資源。(進程池會自己管理)
- 進程的創建是需要消耗大量資源的,進程池很好地解決了這一點。
- 進程如果不多,不要創建進程池。
- 進程池會重複利用進程去做事情。
上圖進程池有三個進程,當某個程序執行完後,下面等待的程序就會被空閒出來的進程取用,從而達到重複利用的效果。
- 進程池使用代碼:
5)複製文件夾
1>明確目標
1.準備工作
2.準備一些供下載的文件
- 我們直接獲取python的標準庫文件
__file__
方法,可以獲得庫的路徑
2>逐步實現
1.大體流程
- 獲取用戶想下載的文件名
- 創建一個同名文件夾
- 獲取待複製文件夾的目錄(打印出來看看對不對)
2.嘗試拷貝
- 如果文件夾已經存在?會報錯,所以使用try
- 爲了多進程,且不知道有多少文件,使用進程池
- 添加拷貝的任務入進程池
3.簡單拷貝
- 怎麼知道拷貝誰,拷貝到哪去?==>添加參數
- 注意:不加join可能會完不成(看不到執行結果)
- 效果:
4.真正拷貝
- 完善打開文件代碼
(別想不開嘗試打印取到的內容,多個進程同時執行,打印在終端是串在一起的)
5.顯示拷貝進度
- 添加顯示進度功能
- 思路:
- 主進程閒着,所以讓主進程來幹
- 可以創建一個隊列,子進程完成後向隊列添加,讓主進程讀取
- 用了進程池。主進程要和子進程通信,不能用multiprocessing.Queue(),而要用multiprocessing.Manager()創建出來的對象下的一個Queue()方法
- 就是multiprocessing.Queue() 變爲 multiprocessing.Manager().Queue()
- 流程
- 創建一個隊列,子進程結束時,向隊列傳入一個消息(任意消息,比如1啊。我這裏用了文件名,一個字符串。)
- 爲了讓主進程,不等待子進程執行完再執行下方代碼,刪除join()
- 主進程使用q.get()來取數據,只要隊列空了,就會阻塞。
- 阻塞需要強制停止(複製完成時)。可以添加兩個變量,一個變量記錄文件列表總數,另一個記錄完成的進程數(利用計數器),添加一個if條件判斷退出。
- 進度 = (完成數/總數)* 100%
- 我這裏執行代碼出現了編碼報錯,因爲中文。所以在第一行添加:
- 完整代碼:
- 結果:(記得先把之前執行代碼產生的復件刪了)
6.進度緩衝在一行
- 能不能打印在一行?結束時不換行就行了(end=""),看結果:
- 誒,怎麼回事?哦!雖然是不換行,但打印是連着打印的,如果打印在行首,就能覆蓋掉之前的打印了。可以使用
\r
回車,不換行完成。 - 還有一個問題,shell輸入的行緊接着打印內容的末尾,所以最後加個換行
- 最終代碼如下(部分)
- 結果:
三、多協程
1)迭代器
1>實現迭代器
- 迭代: 在一個文件的基礎上新增一個大功能,或一個小功能,就叫做迭代。
- 可以迭代的對象,必有
__iter__
類。 - 可以迭代的對象,必是Iterable的子類
- 判斷一個類是否是另一個類的子類,可以用
collections
下的isinstance
方法
- 執行結果:
2>可被for循環的迭代
- 使用for循環時,好像有一個東西來記錄迭代到了哪個元素
- 使用for 遍歷時,其實有兩個步驟。
- 判斷in後面的對象是否可以迭代
- 在第一步的基礎上,調用iter()函數得到一個對象的
__iter__
方法的返回值__iter__
方法的返回值,是一個迭代器- 每for一次,就會調用一次
__next__
方法,返回它的返回值。返回什麼,就看見什麼。
- iter()是一個魔法方法,當調用時,它就把希望有迭代功能的那個類創建的對象,放在裏面傳遞。類似於: iter(user)。然後,自動地調用裏面的
__iter__
方法,得到一個返回值。這個返回的對象,叫做迭代器。 - 如果是迭代器,必是Iterator的子類。
- 代碼如下:
- 結果如下:
3>被遍歷成功時返回的迭代
- next(迭代器對象) 是一個魔法方法,它會調用迭代器對象的
__next__
方法,並返回其返回的結果。 - 每一次for都是調用了一次next()方法。其實list()、tuple()轉換類型,也是先取出,再寫入的過程。
- 可以將希望遍歷的類,作爲參數,傳入迭代器中,以獲得參數
- 使用raise來拋出StopIteration異常以停止遍歷
- 代碼:
- 結果:
4>整合代碼
- 既然,迭代器也含有
__iter__
,說明它是可以被迭代的 - 那麼,我們可以將兩個類,整合變成一個類。
- 注意,
__iter__
返回self即可,因爲自身本來就是迭代器對象 - 代碼:
- 一個對象是迭代器,一定可迭代
- 一個可迭代的對象,不一定是迭代器
5>迭代器的應用場景
- 在一個程序裏,如果需要用到很多值,可以:
- 找個列表之類的存起來
- 什麼時候需要,什麼時候取出
- 這其實是兩種思想,一種是直接存儲結果,一種是存儲得到結果的方法。迭代器存儲的是方法,隨取隨用,省空間。
- 在python2中,range以列表形式存儲結果,xrange存儲的則是方法。如下:
- 在python3中,range實際上爲xrange。
6>迭代器案例:兔子數列
- 定義兩個數0, 1,後面的數分別是其前兩個數的和,這樣一個數列,稱爲斐波那契數列,也稱兔子數列。
- 列表法(代碼):
- 列表法(結果):
- 迭代器法(代碼):
- 迭代器法(結果):
2)生成器
- 生成器是一種特殊的迭代器
- 只要有yield,就是生成器
- 生成器可以保證函數只執行一部分
1>列表生成式變爲生成器
- 只要把列表生成式的
[]
改爲()
即可
2>改造兔子數列
- 只要函數中,出現yield,它就不會被識別爲函數,而是一個生成器對象
- 執行時,每次碰到yield就會暫停,並且返回yield後面的值(這個值可用next方法接收,也可用for等接收)。每調用一次next(),代碼就會繼續執行。
- 代碼:
- 結果:
- 說明yield確實起到了暫停的作用
- 注意: 生成的生成器對象,是相互獨立的
3>return
- 可以在代碼最後添加return關鍵字,需要得到return關鍵字返回的結果。需要通過拋出的異常StopIteration下的value屬性來返回。
- 代碼:
- 結果:
4>send
- 除了next可以啓動生成器外,send也可以
- send可以傳參數入生成器,可以起到控制的作用
生成器.send("傳入的值")
- 代碼:
- 代碼執行完第5行時,發現第6行是個賦值語句,於是先執行等號右邊的,因爲是個yield語句,所以暫停,返回的值,通過next或send或for等取出。同時因爲send或next啓動了生成器,生成器繼續運作。
- send發送了數據,賦值給了等號左邊的變量s。
- 結果:
3)使用yield實現多任務
- 通過生成器讓任務交替執行,形成多線程
- 代碼:
- 協程調用一個任務就像調用一個函數一樣,它調用的資源最少。線程和進程依次增多。
1>使用greenlet升級多協程
-
greenlet是封裝了yield,使其更簡單好用
-
安裝greenlet
sudo pip3 install greenlet
- 代碼:
2>使用gevent升級多協程
- 實際在寫代碼時,常用的是gevent
- greenlet已經實現了協程,但是還得人工切換
- greenlet有一個問題,如果函數中有一個延時,內部根本不會切換,這根本不是多任務。gevent就解決了這點,在延時時,也會進行。
- 安裝gevent
sudo pip3 install gevent
- 代碼:
- 效果:
- 等待一個對象執行完,再執行下一個,這是多任務嗎?有沒有可能是執行地太快了?
- 可以加個sleep延時來驗證是否是多任務,代碼:
- 效果: (說明真的不是多任務)
3>使用gevent完成真正的多協程
- gevent實現多任務需要的延時,不是time.sleep,而是gevent.sleep,代碼如下:
- 效果如下:
- 如果要使用多協程,必須把所有 需要延時 的操作都換爲gevent, 比如socket.connect 要換爲 gevent.connect
- 多進程是創建多個程序,多線程是一個程序裏面創建多個讀取的工具,多協程就是在一個對象內部堵塞時,利用堵塞的時間,去執行其他代碼。
4>使用猴子補丁升級代碼
- 如果一個幾萬行的代碼,全部變爲gevent下延時操作,那不是太繁瑣了。所以,要打個猴子補丁:
- 結果:
- 猴子補丁的作用是:遇到延時操作,將其換成gevent.
5>使用joinall升級代碼
- 一個個join太繁瑣了,可以使用joinall([…])
4)併發下載器
1>從網上下載文件
- 訪問網站,並down下源碼
- 嘗試down一個圖片
我使用的網址是: http://http://222.186.12.239:20012/uploadfile/2019/0916/20190916045739355.jpg
- 代碼如下:
- 效果如下:
2>下載多張圖片
- 代碼:
- 效果:
5)進程、線程、協程
- 進程是資源分配的單位
- 線程是操作系統調度的單位
- 進程切換需要的資源很最大,效率很低
- 線程切換需要的資源一般,效率一般(在不考慮GIL的情況下)
- 協程切換任務資源很小,效率高
- 多進程、多線程根據cpu核數不一樣可能是並行的,但是協程是在一個線程中 所以是併發