1. 程式人生 > >Python學習第35天之程序part1

Python學習第35天之程序part1

一、程序的理論基礎

1、程序就是一個程式在一個數據集上的一次動態執行過程。是用來描述程式執行過程的虛擬概念。程序的概念起源於作業系統,程序是作業系統最核心的概念,作業系統其它所有的概念都是圍繞程序來的。程序一般由程式、資料集、程序控制塊三部分組成。我們編寫的程式用來描述程序要完成哪些功能以及如何完成;資料集則是程式在執行過程中所需要使用的資源;程序控制塊用來記錄程序的外部特徵,描述程序的執行變化過程,系統可以利用它來控制和管理程序,它是系統感知程序存在的唯一標誌

2、程序與程式的區別

  程式僅僅只是一堆程式碼而已,而程序指的是程式的執行過程

  需要強調的是:同一程式執行兩次,那也是程序,比如登入QQ,雖然都是同一個軟體,但是一個可以視訊聊天,一個可以逛空間。

3、併發和並行

併發:單CPU,多程序併發

  無論是並行還是併發,在使用者看來都是 “同時” 執行的,不管是程序還是執行緒,都只是一個任務而已,真實幹活的是 CPU,CPU 來做這些任務,而一個 CPU 同一時刻只能執行一個任務

  併發是偽並行,即看起來是同時執行。單個 CPU + 多道技術就可以實現併發(並行也屬於併發)

並行:多CPU(同時執行,只有具有多個cpu才能實現並行)

  單核下,可以利用多道技術,多個核,每個核也都可以利用多道技術(多道技術是針對單核而言的

  有四個核,六個任務,這樣同一時間有四個任務被執行,假設分別被分配給了 CPU1,CPU2,CPU3,CPU4,一旦任務 1 遇到 I/O 就被迫中斷執行,此時任務 5 就拿到 CPU1 的時間片去執行,這就是單核下的多道技術,而一旦任務 1 的 I/O 結束了,作業系統會重新呼叫它(需知程序的排程、分配給哪個 CPU 執行,由作業系統說了算)

,可能被分配給四個 CPU 中的任意一個去執行

  所有現代計算機經常會在同一時間做很多件事,一個使用者的PC(無論是單 CPU 還是多CPU),都可以同時執行多個任務(一個任務可以理解為一個程序)。

PS:多道技術

多道技術:記憶體中同時存入多道(多個)程式,CPU 從一個程序快速切換到另外一個,使每個程序各自執行幾十或幾百毫秒,這樣,雖然在某一個瞬間,一個 CPU 只能執行一個任務,但在 1 秒內,CPU 卻可以執行多個程序,這就給人產生了並行的錯覺,即偽併發,以此來區分多處理器作業系統的真正硬體並行(多個 CPU 共享同一個實體記憶體)

4、同步和非同步

同步執行:一個程序在執行某個任務時,另外一個程序必須等待其執行完畢,才能繼續執行

非同步執行:一個程序在執行某個任務時,另外一個程序無需等待其執行完畢,就可以繼續執行,當有訊息返回時,系統會通知後者進行處理,這樣可以提高執行效率

舉個例子,打電話時就是同步通訊,發短息時就是非同步通訊。

5、程序的建立

但凡是硬體,都需要有作業系統去管理,只要有作業系統,就有程序的概念,就需要有建立程序的方式,一些作業系統只為一個應用程式設計,比如微波爐中的控制器,一旦啟動微波爐,所有的程序都已經存在。而對於通用系統(跑很多應用程式),需要有系統執行過程中建立或撤銷程序的能力,主要分為四種形式建立新的程序

   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 系統來說,從一開始父程序與子程序的地址空間就是不同的。

6、程序的終止

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

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

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

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

7、程序的層次結構

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

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

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

8、程序的狀態

 

9、程序併發的現象

  程序併發的實現在於,硬體中斷一個正在執行的程序,把此時程序執行的所有狀態儲存下來,為此,作業系統為每個程序定義了一個數據結構——程序控制塊 PCB(Process Control Block)。它是程序實體的一部分,是作業系統中最重要的記錄型資料結構。PCB 中記錄了作業系統所需的、用於描述程序的當前情況以及控制程序執行的全部資訊。程序控制塊的作用是使一個在多道程式環境下不能獨立執行的程式(含資料),成為一個能獨立執行的基本單位,一個能與其它程序併發執行的程序。或者說,OS 是根據 PCB 來對併發執行的程序進行控制和管理的。PCB 是程序存在的唯一標誌。

 

二、程序的使用

1、multiprocessing 模組介紹

Python 中的多執行緒無法利用多核優勢,如果想要充分地使用多核 CPU 的資源(os.cpu_count()檢視),在 Python 中大部分情況需要使用多程序。Python提供了 multiprocessing。

multiprocessing 模組用來開啟子程序,並在子程序中執行我們定製的任務(比如函式),該模組與多執行緒模組 threading 的程式設計介面類似。

multiprocessing 模組的功能眾多:支援子程序、通訊和共享資料、執行不同形式的同步,提供了 Process、Queue、Pipe、Lock 等元件。

需要再次強調的一點是:與執行緒不同,程序沒有任何共享狀態,程序修改的資料,改動僅限於該程序內。

2、Process 類的介紹

(1)建立程序的類

Process([group [, target [, name [, args [, kwargs]]]]]),由該類例項化得到的物件,可用來開啟一個子程序
 
強調:
1. 需要使用關鍵字的方式來指定引數
2. args 指定的為傳給 target 函式的位置引數,是一個元組形式,必須有逗號

(2)、引數介紹

group引數未使用,值始終為None

target表示呼叫物件,即子程序要執行的任務

args表示呼叫物件的位置引數元組,args=(1,2,'qiu',)

kwargs表示呼叫物件的字典,kwargs={'name':'qiu','age':18}

name為子程序的名稱

(3)、方法介紹

p.start():啟動程序,並呼叫該子程序中的 p.run() 
p.run():程序啟動時執行的方法,正是它去呼叫 target 指定的函式,我們自定義類的類中一定要實現該方法  

p.terminate(): 強制終止程序 p,不會進行任何清理操作,如果 p 建立了子程序,該子程序就成了殭屍程序,使用該方法需要特別小心這種情況。
如果 p 還儲存了一個鎖那麼也將不會被釋放,進而導致死鎖
p.is_alive(): 如果 p 仍然執行,返回 True

p.join([timeout]): 主執行緒等待 p 終止(強調:是主執行緒處於等的狀態,而p是處於執行的狀態)。timeout是可選的超時時間,
需要強調的是,p.join只能join住 start 開啟的程序,而不能 join 住 run 開啟的程序

(4)屬性介紹

p.daemon:預設值為 False,如果設為 True,代表 p 為後臺執行的守護程序,當 p 的父程序終止時,p 也隨之終止,並且設定為 True 後,p 不能建立自己的新程序
,必須在 p.start() 之前設定 p.name: 程序的名稱 p.pid:程序的pid p.exitcode:程序在執行時為None、如果為–N,表示被訊號N結束(瞭解即可) p.authkey: 程序的身份驗證鍵,預設是由 os.urandom() 隨機生成的
32 字元的字串。這個鍵的用途是為涉及網路連線的底層程序間通訊提供安全性,
這類連線只有在具有相同的身份驗證鍵時才能成功
(瞭解即可)

3、Process類的使用

注意:在 Windows 中 Process() 必須放到 if __name__ == '__main__': 下

由於Windows沒有fork,多處理模組啟動一個新的Python程序並匯入呼叫模組。
    如果在匯入時呼叫Process(),那麼這將啟動無限繼承的新程序(或直到機器耗盡資源)。
這是隱藏對Process()內部呼叫的原,使用if __name__ == “__main __”,這個if語句
中的語句將不會在匯入時被呼叫。
方式一:
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__':
    p = Process(target=task, args=("qiu",))
    # p = Process(target=task, kwargs={"name": "qiu"})

    # p.start()只是向作業系統傳送了一個開啟子程序的訊號, 作業系統才能開啟子程序,
    # 涉及到申請記憶體空間, 要將父程序的資料拷貝到子程序, 要將CPU調到子程序裡執行子程序的程式碼
    # 才會有 is running的顯示, 這都是一系列的硬體操作
    # 所以print("")這行程式碼執行速度要快一些
    p.start()
    print("")

方式二:

from multiprocessing import Process
import time

class MyProcess(Process):

    def __init__(self, name):
        super().__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("qiu")
    p.start()
    print("")

4、join方法

在主程序執行過程中如果想要併發的執行其他任務,我們可以開啟子程序,此時主程序的任務和子程序的任務分為兩種情況:

  一種情況是:在主程序的任務與子程序的任務彼此獨立的情況下,主程序的任務先執行完畢後,主程序還需要等待子程序執行完畢,然後統一回收資源

  還有一種情況是:如果主程序的任務在執行到某一個階段時,需要等待子程序執行完畢後才能繼續執行,就需要一種機制能夠讓主程序監測子程序是否執行完畢,在子程序執行完畢後才繼續執行,否則一直在原地阻塞,這就是 join 方法的作用。 

from multiprocessing import Process
import time

def task(name, n):
    print("%s is running" %name)
    time.sleep(n)
    print("%s is done" %name)

if __name__ == '__main__':
    p1 = Process(target=task, args=("Process 1", 1))
    p2 = Process(target=task, args=("Process 2", 2))
    p3 = Process(target=task, args=("Process 3", 3))

    start = time.time()
    p1.start()
    p2.start()
    p3.start()

    p1.join()
    p2.join()
    p3.join()
    print("主程序", time.time() - start)

人會有疑問,既然 join 是等待程序結束,那麼我像下面 join 下去,程序不就變成串行了的嗎?

  當然不是了,必須明確 join 是讓誰等:程序只要 start 就會在開始運行了,所以 p1 到 p3.start() 時,系統中已經有三個併發的程序了,而 p1.join() 是在等 p1 結束,p1 只要不結束主執行緒就會一直卡在原地,這也是問題的關鍵。join 是讓主執行緒等,而 p1-p3 仍然是併發執行的,p1.join() 的時候,其餘 p2,p3 仍然在執行,等 p1.join() 結束,可能 p2,p3 早已經結束了,這樣 p2.join(),p3.join() 直接通過檢測,無需等待。所以 3 個 join 花費的總時間仍然是耗費時間最長的那個程序執行的時間,所以這裡即便交換 join 的順序,執行的時間仍然是 3 秒多一點,多出來的那零點幾秒是開啟程序以及程序切換的時間。

交換順序

from multiprocessing import Process
import time

def task(name, n):
    print("%s is running" %name)
    time.sleep(n)
    print("%s is done" %name)

if __name__ == '__main__':
    p1 = Process(target=task, args=("Process 1", 1))
    p2 = Process(target=task, args=("Process 2", 2))
    p3 = Process(target=task, args=("Process 3", 3))

    start = time.time()
    p1.start()
    p2.start()
    p3.start()

    p3.join()
    p1.join()
    p2.join()

    print("主程序", time.time() - start)

join 是讓主程序在原地等待,等待子程序執行完畢,不會影響子程序的執行

5、程序間的記憶體空間互相隔離

from multiprocessing import Process

n = 100

def task():
    global n
    n = 0

if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    p.join()
    print("主程序內的:", n)

6、殭屍程序與孤兒程序

  殭屍程序:一個程序使用 fork 建立子程序,如果子程序退出,而父程序並沒有呼叫 wait 或 waitpid 獲取子程序的狀態資訊,那麼子程序的程序描述符仍然儲存在系統中。這種程序稱之為僵死程序

  我們知道在 Unix/Linux 中,正常情況下子程序是通過父程序建立的,子程序在建立新的程序。子程序的結束和父程序的執行是一個非同步過程,即父程序永遠無法預測子程序到底什麼時候結束,如果子程序一結束就立刻回收其全部資源,那麼在父程序內將無法獲取子程序的狀態資訊。因此,Unix 提供了一種機制可以保證父程序可以在任意時刻獲取子程序結束時的狀態資訊:

  1、在每個程序退出的時候,核心釋放該程序所有的資源,包括開啟的檔案,佔用的記憶體等。但是仍然為其保留一定的資訊(包括程序號、退出狀態、執行時間等)

  2、直到父程序通過 wait/waitpid 來取時才釋放。但這樣就導致了問題,如果程序不呼叫 wait/waitpid 的話,那麼保留的那段資訊就不會釋放,其程序號就會一直被佔用,但是系統所能使用的程序號是有限的,如果大量的產生僵死程序,將因為沒有可用的程序號而導致系統不能產生新的程序。此即為殭屍程序的危害,應當避免。

  任何一個子程序(init除外)在 exit() 之後,並非馬上就消失掉,而是留下一個稱為殭屍程序(Zombie)的資料結構,等待父程序處理。這是每個子程序在結束時都要經過的階段。如果子程序在 exit() 之後,父程序沒有來得及處理,這時用 ps 命令就能看到子程序的狀態是 “Z” 。如果父程序能及時 處理,可能用 ps 命令就來不及看到子程序的殭屍狀態,但這並不等於子程序不經過殭屍狀態。  如果父程序在子程序結束之前退出,則子程序將由 init 接管。init 將會以父程序的身份對殭屍狀態的子程序進行處理。

  孤兒程序:一個父程序退出,而它的一個或多個子程序還在執行,那麼那些子程序將成為孤兒程序。孤兒程序將被 init 程序(程序號為 1)所收養,並由 init 程序對它們完成狀態收集工作。

  孤兒程序是沒有父程序的程序,孤兒程序這個重任就落到了 init 程序身上,init 程序就好像是一個民政局,專門負責處理孤兒程序的善後工作。每當出現一個孤兒程序的時候,核心就把孤 兒程序的父程序設定為 init,而 init 程序會迴圈地 wait() 它的已經退出的子程序。這樣,當一個孤兒程序淒涼地結束了其生命週期的時候,init 程序就會代表黨和政府出面處理它的一切善後工作。因此孤兒程序並不會有什麼危害。