多任務 #多線程 #多協程 #多進程 #併發

  • 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,其實可以分爲許多步
  1. 找到變量
  2. 更改變量引用
  3. 存儲變量引用
  • 然後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)進程補充

  • 多進程是什麼?
  • 進程就是代碼加資源。多進程就是,把資源複製一份,然後指定了代碼的開始位置。(代碼不復制是因爲,代碼不會變,是共享的)
  • 進程耗費的資源大
  • 進程浪費了內存,但是提高了效率(一定範圍內的,如果進程數過多,會卡)
  • 原則上,能共享就共享。實在當,通過特殊手段修改代碼時,代碼纔會複製。這叫寫時拷貝

進程與線程的區別

  • 先有進程,纔有線程。
  • 進程僅僅是一個資源分配的單位,資源單位的總和,上面的線程拿的資源最多。
  • 線程是進程調度和分配的基本單位。
  • 多線程就是同一個資源裏有多個執行代碼的東西。
  • 多進程就是有多個資源。

  • 區別:
  1. 線程的劃分尺度小於進程(資源比進程少),使得多線程程序的併發性高。
  2. 一個程序至少有一個進程,一個進程至少有一個線程.
  3. 進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率
  • 線程和進程在使用上各有優缺點:線程執行開銷小,但不利於資源的管理和保護;而進程正相反。

3)進程間通信

  • 線程間資源是共享的,美滋滋。但是進程間不是,需要用對方的資源,必須通過通信。
  • 進程間通信,有很多種機制:
  1. 進程間通信,可以用socket。
  2. 進程間通信,可以用文件(文件在硬盤上,所以速度較慢)。
  3. 可以設想,如果在內存中開闢一塊地方,進程把數據存到這個內存中,另一個進程訪問這塊內存就完成了通信。
  • Queue隊列
  • 先進先出,就叫隊列
  • 用Queue的一個主要目的是:解耦

耦合性高,有可能,改了一小塊代碼,其它一大片地方要跟着改,不然程序就完蛋了。

  • 隊列使用的代碼:
  • 需要在創建進程前就創建好一個隊列。
  • 實際操作時代碼如下:
    在這裏插入圖片描述
  • 效果如下
    在這裏插入圖片描述

4)進程池Pool

  • 進程池,就是先創建一個池,裏面有許多進程,先執行一部分進程,然後某些進程結束了,新的進程通過重複利用,來提高效率,節省資源。(進程池會自己管理)
  • 進程的創建是需要消耗大量資源的,進程池很好地解決了這一點。
  • 進程如果不多,不要創建進程池。
  • 進程池會重複利用進程去做事情。
    在這裏插入圖片描述

上圖進程池有三個進程,當某個程序執行完後,下面等待的程序就會被空閒出來的進程取用,從而達到重複利用的效果。

  • 進程池使用代碼:
    在這裏插入圖片描述

5)複製文件夾

1>明確目標

1.準備工作

在這裏插入圖片描述

2.準備一些供下載的文件

  • 我們直接獲取python的標準庫文件
  • __file__方法,可以獲得庫的路徑
    在這裏插入圖片描述

2>逐步實現

1.大體流程

  • 獲取用戶想下載的文件名
  • 創建一個同名文件夾
  • 獲取待複製文件夾的目錄(打印出來看看對不對)
    在這裏插入圖片描述

2.嘗試拷貝

  • 如果文件夾已經存在?會報錯,所以使用try
  • 爲了多進程,且不知道有多少文件,使用進程池
  • 添加拷貝的任務入進程池
    在這裏插入圖片描述

3.簡單拷貝

  • 怎麼知道拷貝誰,拷貝到哪去?==>添加參數
  • 注意:不加join可能會完不成(看不到執行結果)
    在這裏插入圖片描述
  • 效果:
    在這裏插入圖片描述

4.真正拷貝

  • 完善打開文件代碼
    (別想不開嘗試打印取到的內容,多個進程同時執行,打印在終端是串在一起的)
    在這裏插入圖片描述

5.顯示拷貝進度

  • 添加顯示進度功能
  1. 思路:
  • 主進程閒着,所以讓主進程來幹
  • 可以創建一個隊列,子進程完成後向隊列添加,讓主進程讀取
  • 用了進程池。主進程要和子進程通信,不能用multiprocessing.Queue(),而要用multiprocessing.Manager()創建出來的對象下的一個Queue()方法
  • 就是multiprocessing.Queue() 變爲 multiprocessing.Manager().Queue()
  1. 流程
  • 創建一個隊列,子進程結束時,向隊列傳入一個消息(任意消息,比如1啊。我這裏用了文件名,一個字符串。)
  • 爲了讓主進程,不等待子進程執行完再執行下方代碼,刪除join()
  • 主進程使用q.get()來取數據,只要隊列空了,就會阻塞。
  • 阻塞需要強制停止(複製完成時)。可以添加兩個變量,一個變量記錄文件列表總數,另一個記錄完成的進程數(利用計數器),添加一個if條件判斷退出。
  • 進度 = (完成數/總數)* 100%
  • 我這裏執行代碼出現了編碼報錯,因爲中文。所以在第一行添加:在這裏插入圖片描述
  • 完整代碼:
    在這裏插入圖片描述
    在這裏插入圖片描述
  • 結果:(記得先把之前執行代碼產生的復件刪了)
    -

6.進度緩衝在一行

  • 能不能打印在一行?結束時不換行就行了(end=""),看結果:
    在這裏插入圖片描述
  • 誒,怎麼回事?哦!雖然是不換行,但打印是連着打印的,如果打印在行首,就能覆蓋掉之前的打印了。可以使用\r回車,不換行完成。
  • 還有一個問題,shell輸入的行緊接着打印內容的末尾,所以最後加個換行
  • 最終代碼如下(部分)
    在這裏插入圖片描述
  • 結果:
    在這裏插入圖片描述

三、多協程

1)迭代器

1>實現迭代器

  • 迭代: 在一個文件的基礎上新增一個大功能,或一個小功能,就叫做迭代。
  • 可以迭代的對象,必有__iter__類。
  • 可以迭代的對象,必是Iterable的子類
  • 判斷一個類是否是另一個類的子類,可以用collections下的isinstance方法
    在這裏插入圖片描述
  • 執行結果:
    在這裏插入圖片描述

2>可被for循環的迭代

  • 使用for循環時,好像有一個東西來記錄迭代到了哪個元素
  • 使用for 遍歷時,其實有兩個步驟。
  1. 判斷in後面的對象是否可以迭代
  2. 在第一步的基礎上,調用iter()函數得到一個對象的__iter__方法的返回值
  3. __iter__方法的返回值,是一個迭代器
  4. 每for一次,就會調用一次__next__方法,返回它的返回值。返回什麼,就看見什麼。
  • iter()是一個魔法方法,當調用時,它就把希望有迭代功能的那個類創建的對象,放在裏面傳遞。類似於: iter(user)。然後,自動地調用裏面的__iter__方法,得到一個返回值。這個返回的對象,叫做迭代器。
  • 如果是迭代器,必是Iterator的子類。
  • 代碼如下:
    在這裏插入圖片描述
  • 結果如下:
    在這裏插入圖片描述

3>被遍歷成功時返回的迭代

  • next(迭代器對象) 是一個魔法方法,它會調用迭代器對象的__next__方法,並返回其返回的結果。
  • 每一次for都是調用了一次next()方法。其實list()、tuple()轉換類型,也是先取出,再寫入的過程。
  • 可以將希望遍歷的類,作爲參數,傳入迭代器中,以獲得參數
  • 使用raise來拋出StopIteration異常以停止遍歷
  • 代碼:
    在這裏插入圖片描述
    在這裏插入圖片描述
  • 結果:
    在這裏插入圖片描述

4>整合代碼

  • 既然,迭代器也含有__iter__,說明它是可以被迭代的
  • 那麼,我們可以將兩個類,整合變成一個類。
  • 注意,__iter__返回self即可,因爲自身本來就是迭代器對象
  • 代碼:
    在這裏插入圖片描述
  • 一個對象是迭代器,一定可迭代
  • 一個可迭代的對象,不一定是迭代器

5>迭代器的應用場景

  • 在一個程序裏,如果需要用到很多值,可以:
  1. 找個列表之類的存起來
  2. 什麼時候需要,什麼時候取出
  • 這其實是兩種思想,一種是直接存儲結果,一種是存儲得到結果的方法。迭代器存儲的是方法,隨取隨用,省空間。
  • 在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>從網上下載文件

  1. 訪問網站,並down下源碼
  2. 嘗試down一個圖片

我使用的網址是: http://http://222.186.12.239:20012/uploadfile/2019/0916/20190916045739355.jpg

  • 代碼如下:
    在這裏插入圖片描述
  • 效果如下:
    在這裏插入圖片描述

2>下載多張圖片

  • 代碼:
    在這裏插入圖片描述
  • 效果:
    在這裏插入圖片描述

5)進程、線程、協程

  1. 進程是資源分配的單位
  2. 線程是操作系統調度的單位
  3. 進程切換需要的資源很最大,效率很低
  4. 線程切換需要的資源一般,效率一般(在不考慮GIL的情況下)
  5. 協程切換任務資源很小,效率高
  6. 多進程、多線程根據cpu核數不一樣可能是並行的,但是協程是在一個線程中 所以是併發
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章