1. 程式人生 > >Python學習--day31-併發程式設計

Python學習--day31-併發程式設計

day 31

程序

一、什麼是程序:

程序指的是正在執行的程式,是一系列過程的統稱,也是作業系統排程和進行資源分配的基本單位。

程序是實現併發的一種方式,在學習併發程式設計之前要先了解程序的基本概念以及多程序的實現原理,這就必須提到作業系統了,因為程序這個概念來自於作業系統,沒有作業系統就沒有程序。

二、什麼是併發程式設計:

併發指的是多個任務同時被執行,在之前的TCP通訊中,伺服器在建立連線後需要一個迴圈來與客戶端迴圈的收發資料,但伺服器並不知道客戶端什麼時候會發來資料,導致沒有數時伺服器進入了一個等待狀態,此時其他客戶端也無法連結伺服器,很明顯這是不合理的,學習併發程式設計就是要找到一種方案,讓一個程式中的的多個任務可以同時被處理。

三、多程序的實現原理-多道技術

1、作業系統介紹

下圖是作業系統在整個計算機中所在的位置:

 

作業系統位於應用軟體和硬體裝置之間,本質上也是一個軟體,

它由系統核心(管理所有硬體資源)與系統介面(提供給程式設計師使用的介面)組成

作業系統是為了方便使用者操作計算機而提供的一個執行在硬體之上的軟體

2、作業系統的兩個核心作用

1、為使用者遮蔽了複雜繁瑣的硬體介面,為應用程式提供了,清晰易用的系統介面。

有了這些介面以後程式設計師不用再直接與硬體打交道了

例子:有了作業系統後我們就可以使用資源管理器來操作硬碟上的資料,而不用操心,磁頭的移動啊,資料的讀寫等等

2、作業系統將應用程式對硬體資源的競爭變成了有序的使用。

例子:所有軟體 qq啊 微信啊 吃雞啊都共用一套硬體裝置 假設現有三個程式都在使用印表機,如果不能妥善管理競爭問題,可能一個程式列印了一半圖片後,另一個程式搶到了印表機執行權於是列印了一半文字,導致兩個程式的任務都沒能完成,作業系統的任務就是將這些無序的操作變得有序

3、作業系統與應用程式的區別

二者的區別不在於的地位,它們都是軟體,而作業系統可以看做一款特殊的軟體

1.作業系統是受保護的:無法被使用者修改(應用軟體如qq不屬於作業系統可以隨便解除安裝)。

2.大型:linux或widows原始碼都在五百萬行以上,這僅僅是核心,不包括使用者程式,如GUI,庫以及基本應用軟體(如windows Explorer等),很容易就能達到這個數量的10倍或者20倍之多。

3.長壽:由於作業系統原始碼量巨大,編寫是非常耗時耗力的,一旦完成,作業系統所有者便不會輕易的放棄重寫,二是在原有基礎上改進,基本上可以把windows95/98/Me看成一個作業系統。

發展歷史:

多道技術出現在第三代作業系統中,是為了解決前兩代作業系統存在的種種問題而出現,那麼前兩代作業系統都有哪些問題呢?一起來看看作業系統的發展歷史:

第一代計算機(1940~1955):真空管和穿孔卡片

第一代計算機的產生背景:

第一代之前人類是想用機械取代人力,第一代計算機的產生是計算機由機械時代進入電子時代的標誌,從Babbage失敗之後一直到第二次世界大戰,數字計算機的建造幾乎沒有什麼進展,第二次世界大戰刺激了有關計算機研究的爆炸性進展。

lowa州立大學的john Atanasoff教授和他的學生Clifford Berry建造了據認為是第一臺可工作的數字計算機。該機器使用300個真空管。大約在同時,Konrad Zuse在柏林用繼電器構建了Z3計算機,英格蘭布萊切利園的一個小組在1944年構建了Colossus,Howard Aiken在哈佛大學建造了Mark 1,賓夕法尼亞大學的William Mauchley和他的學生J.Presper Eckert建造了ENIAC。這些機器有的是二進位制的,有的使用真空管,有的是可程式設計的,但都非常原始,設定需要花費數秒鐘時間才能完成最簡單的運算。

在這個時期,同一個小組裡的工程師們,設計、建造、程式設計、操作及維護同一臺機器,所有的程式設計是用純粹的機器語言編寫的,甚至更糟糕,需要通過成千上萬根電纜接到外掛板上連成電路來控制機器的基本功能。沒有程式設計語言(彙編也沒有),作業系統則是從來都沒聽說過。使用機器的過程更加原始,詳見下‘工作過程’

特點: 沒有作業系統的概念 所有的程式設計都是直接操控硬體

工作過程: 程式設計師在牆上的機時表預約一段時間,然後程式設計師拿著他的外掛版到機房裡,將自己的外掛板街道計算機裡,這幾個小時內他獨享整個計算機資源,後面的一批人都得等著(兩萬多個真空管經常會有被燒壞的情況出現)。

後來出現了穿孔卡片,可以將程式寫在卡片上,然後讀入機而不用外掛板

優點:

程式設計師在申請的時間段內獨享整個資源,可以即時地除錯自己的程式(有bug可以立刻處理)

缺點:

浪費計算機資源,一個時間段內只有一個人用。 注意:同一時刻只有一個程式在記憶體中,被cpu呼叫執行,比方說10個程式的執行,是序列的

第二代計算機(1955~1965):電晶體和批處理系統

第二代計算機的產生背景:

由於當時的計算機非常昂貴,自認很自然的想辦法較少機時的浪費。通常採用的方法就是批處理系統。

特點: 設計人員、生產人員、操作人員、程式人員和維護人員直接有了明確的分工,計算機被鎖在專用空調房間中,由專業操作人員執行,這便是‘大型機’。

有了作業系統的概念

有了程式設計語言:FORTRAN語言或組合語言,寫到紙上,然後穿孔打成卡片,再將卡片盒帶到輸入室,交給操作員,然後喝著咖啡等待輸出介面

工作過程:

第二代如何解決第一代的問題/缺點: 1.把一堆人的輸入攢成一大波輸入, 2.然後順序計算(這是有問題的,但是第二代計算也沒有解決) 3.把一堆人的輸出攢成一大波輸出

現代作業系統的前身:(見圖)

優點:批處理,節省了機時

缺點:1.整個流程需要人蔘與控制,將磁帶搬來搬去(中間倆小人)

2.計算的過程仍然是順序計算-》序列

3.程式設計師原來獨享一段時間的計算機,現在必須被統一規劃到一批作業中,等待結果和重新除錯的過程都需要等同批次的其他程式都運作完才可以(這極大的影響了程式的開發效率,無法及時除錯程式)

第三代計算機(1965~1980):積體電路晶片和多道程式設計

第三代計算機的產生背景:

20世紀60年代初期,大多數計算機廠商都有兩條完全不相容的生產線。

一條是面向字的:大型的科學計算機,如IBM 7094,見上圖,主要用於科學計算和工程計算

另外一條是面向字元的:商用計算機,如IBM 1401,見上圖,主要用於銀行和保險公司從事磁帶歸檔和列印服務

開發和維護完全不同的產品是昂貴的,同時不同的使用者對計算機的用途不同。

IBM公司試圖通過引入system/360系列來同時滿足科學計算和商業計算,360系列低檔機與1401相當,高檔機比7094功能強很多,不同的效能賣不同的價格

360是第一個採用了(小規模)晶片(積體電路)的主流機型,與採用電晶體的第二代計算機相比,價效比有了很大的提高。這些計算機的後代仍在大型的計算機中心裡使用,此乃現在伺服器的前身,這些伺服器每秒處理不小於千次的請求。

如何解決第二代計算機的問題1: 卡片被拿到機房後能夠很快的將作業從卡片讀入磁碟,於是任何時刻當一個作業結束時,作業系統就能將一個作業從磁帶讀出,裝進空出來的記憶體區域執行,這種技術叫做 同時的外部裝置聯機操作:SPOOLING,該技術同時用於輸出。當採用了這種技術後,就不在需要IBM1401機了,也不必將磁帶搬來搬去了(中間倆小人不再需要)

如何解決第二代計算機的問題2:

第三代計算機的作業系統廣泛應用了第二代計算機的作業系統沒有的關鍵技術:多道技術

cpu在執行一個任務的過程中,若需要操作硬碟,則傳送操作硬碟的指令,指令一旦發出,硬碟上的機械手臂滑動讀取資料到記憶體中,這一段時間,cpu需要等待,時間可能很短,但對於cpu來說已經很長很長,長到可以讓cpu做很多其他的任務,如果我們讓cpu在這段時間內切換到去做其他的任務,這樣cpu不就充分利用了嗎。這正是多道技術產生的技術背景

多道技術:

多道技術中的多道指的是多個程式,多道技術的實現是為了解決多個程式競爭或者說共享同一個資源(比如cpu)的有序排程問題,解決方式即多路複用,多路複用分為時間上的複用和空間上的複用。

空間上的複用:將記憶體分為幾部分,每個部分放入一個程式,這樣,同一時間記憶體中就有了多道程式。

 

時間上的複用:當一個程式在等待I/O時,另一個程式可以使用cpu,如果記憶體中可以同時存放足夠多的作業,則cpu的利用率可以接近100%,類似於我們小學數學所學的統籌方法。(作業系統採用了多道技術後,可以控制程序的切換,或者說程序之間去爭搶cpu的執行許可權。這種切換不僅會在一個程序遇到io時進行,一個程序佔用cpu時間過長也會切換,或者說被作業系統奪走cpu的執行許可權)

空間上的複用最大的問題是:程式之間的記憶體必須分割,這種分割需要在硬體層面實現,由作業系統控制。如果記憶體彼此不分割,則一個程式可以訪問另外一個程式的記憶體,

首先喪失的是安全性,比如你的qq程式可以訪問作業系統的記憶體,這意味著你的qq可以拿到作業系統的所有許可權。

其次喪失的是穩定性,某個程式崩潰時有可能把別的程式的記憶體也給回收了,比方說把作業系統的記憶體給回收了,則作業系統崩潰。

多道技術案例:

生活中我們程序會同時做多個任務,但是本質上一個人是不可能同時做執行多個任務的,

例1:吃飯和打遊戲,同時執行,本質上是在兩個任務之間切換執行,吃一口飯然後打打遊戲,打會兒遊戲再吃一口飯;

例2:做飯和洗衣服,如果沒有多道技術,在電飯煲做飯的時候我們就只能等著,假設洗米花費5分鐘,煮飯花費40分鐘,相當於40分鐘是被浪費的時間。那就可以在煮飯的等待過程中去洗衣服,假設把衣服裝進洗衣機花費5分鐘,洗衣服花費40分鐘,那麼總耗時為 5(洗米)+5(裝衣服)+40(最長等待時間) 大大提高了工作效率

 

多道技術也是在不同任務間切換執行,由於計算機的切換速度非常快,所以使用者是沒有任何感覺的,看起來就像是兩個任務都在執行,但是另一個問題是,僅僅是切換還不行,還需要在切換前儲存當前狀態,切換回來時恢復狀態,這些切換和儲存都是需要花費時間的!在上述案例中由於任務過程中出現了等待即IO操作所以進行了切換,而對於一些不會出現IO操作的程式而言,切換不僅不能提高效率,反而會降低效率

例如:做一百道乘法題和做一百道除法題,兩個任務都是計算任務是不需要等待的,此時的切換反而降低了執行效率!

 

 

第三代計算機的作業系統仍然是批處理

許多程式設計師懷念第一代獨享的計算機,可以即時除錯自己的程式。為了滿足程式設計師們很快可以得到響應,出現了分時作業系統

如何解決第二代計算機的問題3:

分時作業系統: 多個聯機終端+多道技術

20個客戶端同時載入到記憶體,有17在思考,3個在執行,cpu就採用多道的方式處理記憶體中的這3個程式,由於客戶提交的一般都是簡短的指令而且很少有耗時長的,索引計算機能夠為許多使用者提供快速的互動式服務,所有的使用者都以為自己獨享了計算機資源

CTTS:麻省理工(MIT)在一臺改裝過的7094機上開發成功的,CTSS相容分時系統,第三代計算機廣泛採用了必須的保護硬體(程式之間的記憶體彼此隔離)之後,分時系統才開始流行

MIT,貝爾實驗室和通用電氣在CTTS成功研製後決定開發能夠同時支援上百終端的MULTICS(其設計者著眼於建造滿足波士頓地區所有使用者計算需求的一臺機器),很明顯真是要上天啊,最後摔死了。

後來一位參加過MULTICS研製的貝爾實驗室電腦科學家Ken Thompson開發了一個簡易的,單使用者版本的MULTICS,這就是後來的UNIX系統。基於它衍生了很多其他的Unix版本,為了使程式能在任何版本的unix上執行,IEEE提出了一個unix標準,即posix(可移植的作業系統介面Portable Operating System Interface)

後來,在1987年,出現了一個UNIX的小型克隆,即minix,用於教學使用。芬蘭學生Linus Torvalds基於它編寫了Linux

第四代計算機(1980~至今):個人計算機

第四代也就是我們常見的作業系統,大多是具備圖形化介面的,例如:Windows,macOS ,CentOS等

由於採用了IC設計,計算機的體積下降,效能增長,並且成本以及可以被普通消費者接受,而第三代作業系統大都需要進行專業的學習才能使用,於是各個大佬公司開始開發那種不需要專業學習也可以快速上手的作業系統,即上述作業系統!

它們都是用了GUI 圖形化使用者介面,使用者只需要通過滑鼠點選拖拽介面上的元素即可完成大部分操作

 

四、程序與程式

程序是正在執行的程式,程式是程式設計師編寫的一堆程式碼,也就是一堆字元,當這堆程式碼被系統載入到記憶體中並執行時,就有了程序。

例如:生活中我們會按照菜譜來做菜,那麼菜譜就是程式,做菜的過程就是程序

需要注意的是:一個程式是可以產生多個程序的,就像我們可以同時執行多個QQ程式一樣,會形成多個程序

測試:

import time
while True:
    time.sleep(1)

 

多次執行該檔案,就會產生多個python.exe程序,可以通過tasklist來檢視執行的程式

PID和PPID
1、PID

在一個作業系統中通常都會執行多個應用程式,也就是多個程序,那麼如何來區分程序呢?

系統會給每一個程序分配一個程序編號即PID,如同人需要一個身份證號來區分。

驗證:

tasklist 用於檢視所有的程序資訊

taskkill /f /pid pid 該命令可以用於結束指定程序

# 在python中可以使用os模組來獲取pid
import os
print(os.getpid())
​
#還可以通過current_process模組來獲得
current_process().pid   #可獲得當前執行的程式的pid

 

2、PPID

當一個程序a開啟了另一個程序b時,a稱為b的父程序,b稱為a的子程序

在python中可以通過os模組來獲取父程序的pid

# 在python中可以使用os模組來獲取ppid
import os
print("self",os.getpid()) # 當前程序自己的pid
print("parent",os.getppid()) # 當前程序的父程序的pid

 

如果是在pycharm中執行的py檔案,那pycahrm就是這個python.exe的父程序,當然你可以從cmd中來執行py檔案,那此時cmd就是python.exe的父程序

 

五、併發與並行,阻塞與非阻塞

1、併發指的是,多個事件同時發生了。

例如洗衣服和做飯,同時發生了,但本質上是兩個任務在切換,給人的感覺是同時在進行,也被稱為偽並行

2、並行指的是,多個事件都是進行著

例如一個人在寫程式碼另一個人在寫書,這兩件事件是同時在進行的,要注意的是一個人是無法真正的並行執行任務的,在計算機中單核CPU也是無法真正並行的,之所以單核CPU也能同時執行qq和微信其實就是併發執行

3、阻塞與非阻塞指的是程式的狀態

阻塞狀態是因為程式遇到了IO操作,或是sleep,導致後續的程式碼不能被CPU執行

非阻塞與之相反,表示程式正在正常被CPU執行

 

補充:程序有三種狀態

就緒態,執行態,和阻塞態

多道技術會在程序執行時間過長或遇到IO時自動切換其他程序,意味著IO操作與,程序被剝奪CPU執行權都會造成程序阻塞

 

 

六、程序相關理論(瞭解)

1、程序的建立

但凡是硬體,都需要有作業系統去管理,只要有作業系統,就有程序的概念,就需要有建立程序的方式,一些作業系統只為一個應用程式設計,比如微波爐中的控制器,一旦啟動微波爐,程序就已經存在。

  而對於通用系統(跑很多應用程式),需要有系統執行過程中建立或撤銷程序的能力,主要分為4中形式建立新的程序

  1. 系統初始化(檢視程序linux中用ps命令,windows中用工作管理員,前臺程序負責與使用者互動,後臺執行的程序與使用者無關,執行在後臺並且只在需要時才喚醒的程序,稱為守護程序,如電子郵件、web頁面、新聞、列印)

  2. 一個程序在執行過程中開啟了子程序(如nginx開啟多程序,os.fork,subprocess.Popen等)

  3. 使用者的互動式請求,而建立一個新程序(如使用者雙擊暴風影音)

  4. 一個批處理作業的初始化(只在大型機的批處理系統中應用)

  

  無論哪一種,新程序的建立都是由一個已經存在的程序執行了一個用於建立程序的系統呼叫而建立的:

  1. 在UNIX中該系統呼叫是:fork,fork會建立一個與父程序一模一樣的副本,二者有相同的儲存映像、同樣的環境字串和同樣的開啟檔案(在shell直譯器程序中,執行一個命令就會建立一個子程序)

  2. 在windows中該系統呼叫是:CreateProcess,CreateProcess既處理程序的建立,也負責把正確的程式裝入新程序。

 

  關於建立的子程序,UNIX和windows

  1.相同的是:程序建立後,父程序和子程序有各自不同的地址空間(多道技術要求物理層面實現程序之間記憶體的隔離),任何一個程序的在其地址空間中的修改都不會影響到另外一個程序。

  2.不同的是:在UNIX中,子程序的初始地址空間是父程序的一個副本,提示:子程序和父程序是可以有隻讀的共享記憶體區的。但是對於windows系統來說,會重新載入程式程式碼。

 

2、程序的銷燬
  1. 正常退出(自願,如使用者點選互動式頁面的叉號,或程式執行完畢呼叫發起系統呼叫正常退出,在linux中用exit,在windows中用ExitProcess)

  2. 出錯退出(自願,python a.py中a.py不存在)

  3. 嚴重錯誤(非自願,執行非法指令,如引用不存在的記憶體,1/0等,可以捕捉異常,try...except...)

  4. 被其他程序殺死(非自願,如kill -9)

3、程序的層次結構

無論UNIX還是windows,程序只有一個父程序,不同的是:

  1. 在UNIX中所有的程序,都是以init程序為根,組成樹形結構。父子程序共同組成一個程序組,這樣,當從鍵盤發出一個訊號時,該訊號被送給當前與鍵盤相關的程序組中的所有成員。

  2. 在windows中,沒有程序層次的概念,所有的程序都是地位相同的,唯一類似於程序層次的暗示,是在建立程序時,父程序得到一個特別的令牌(稱為控制代碼),該控制代碼可以用來控制子程序,但是父程序有權把該控制代碼傳給其他子程序,這樣就沒有層次了。

七、python中實現併發

在一個應用程式中可能會有多個任務需要併發執行,但是對於作業系統而言,一個程序就是一個任務CPU會從上往下依次執行程式碼,現在我們可以通過互動式方式來啟動一個程序,但這並適用於我們的程式,我們不可能要求使用者手動來啟動程序來玩任務,太low!

1、python中開啟子程序的兩種方式

方式1:

例項化Process類

from multiprocessing import Process
import time
​
def task(name):
    print('%s is running' %name)
    time.sleep(3)
    print('%s is done' %name)
if __name__ == '__main__':
    # 在windows系統之上,開啟子程序的操作一定要放到這下面
    # Process(target=task,kwargs={'name':'egon'})
    p=Process(target=task,args=('jack',))
    p.start() # 向作業系統傳送請求,作業系統會申請記憶體空間,然後把父程序的資料拷貝給子程序,作為子程序的初始狀態
    print('======主')

 

方式2:

繼承Process類 並覆蓋run方法

from multiprocessing import Process
import time
​
class MyProcess(Process):
    def __init__(self,name):
        super(MyProcess,self).__init__()
        self.name=name
​
    def run(self):
        print('%s is running' %self.name)
        time.sleep(3)
        print('%s is done' %self.name)
if __name__ == '__main__':
    p=MyProcess('jack')
    p.start()
    print('')

 

需要注意的是 在windows下 開啟子程序必須放到__main__下面,因為windows在開啟子程序時會重新載入所有的程式碼造成遞迴

 

2、程序間記憶體相互隔離
from multiprocessing import Process
import time
x=1000
def task():
    global x
    x=0
    print('兒子死啦',x)
​
​
if __name__ == '__main_
    print(x)
    p=Process(target=task)
    p.start()
    time.sleep(5)
    print(x)

 

3、join函式

呼叫start函式後的操作就由作業系統來玩了,至於何時開啟程序,程序何時執行,何時結束都與應用程式無關,所以當前程序會繼續往下執行,join函式就可以是父程序等待子程序結束後繼續執行

jion函式也是有效結局殭屍程序的一個手段

案例1:

from multiprocessing import Process
import time
​
x=1000def task():
    time.sleep(3)
    global x
    x=0
    print('兒子死啦',x)
if __name__ == '__main__':
    p=Process(target=task)
    p.start()
​
    p.join() # 讓父親在原地等
    print(x)

 

案例2:

from multiprocessing import Process
import time,random
​
x=1000def task(n):
    print('%s is runing' %n)
    time.sleep(n)
​
if __name__ == '__main__':
    start_time=time.time()
​
    p1=Process(target=task,args=(1,))
    p2=Process(target=task,args=(2,))
    p3=Process(target=task,args=(3,))
    p1.start()
    p2.start()
    p3.start()
​
    p3.join() #3s
    p1.join()
    p2.join()
​
    print('',(time.time() - start_time))
​
    start_time=time.time()
    p_l=[]
    for i in range(1,4):
        p=Process(target=task,args=(i,))
        p_l.append(p)
        p.start()
    
    for p in p_l:
        p.join()
    
    print('',(time.time() - start_time))

 

4、Process物件常用屬性
from multiprocessing import Process
def task(n):
    print('%s is runing' %n)
    time.sleep(n)
​
if __name__ == '__main__':
    start_time=time.time()
​
    p1=Process(target=task,args=(1,),name='任務1')
    p1.start()
​
    print(p1.pid)
    print(p1.name)
    p1.terminate()   #終止子程序的命令
    p1.join()    #如果不加這步,下面一行程式碼的結果會顯示True,這是因為,終止子程序需要花時間,cpu自動執行主程序的程式碼去了(下一行程式碼屬於主程序)
    print(p1.is_alive())
​
    print('')

 


5、殭屍程序與孤兒程序:

a、殭屍程序: 是指子程序執行完成所有任務後已經終止了,但是還殘留一些資訊(程序id 程序名) 但是父程序 沒有去處理這些殘留資訊就導致殘留資訊佔用系統記憶體。 殭屍程序時有害的

解決殭屍程序的方案:

1、結束父程序,這樣會使殭屍程序變成孤兒程序,讓作業系統接管。

2、在父程序中新增上處理子程序的處理方法,如加上join函式。

b、孤兒程序:是指父程序已經終止了,但是自己還在執行,它是無害的 孤兒程序會自動作業系統接管,因此作業系統會處理器遺留資訊。