1. 程式人生 > 實用技巧 >第二十五天

第二十五天

引子

  • 併發程式設計

  • socketserver模組實現併發

  • 基於TCP協議的套接字---支援併發

  • 基於UDP協議的套接字---支援併發

  • 作業系統

  • 程序相關概念

  • 開啟程序的兩種方式

  • 程序之間記憶體空間彼此隔離

  • 程序物件的方法

  • socketserver模組實現併發

    基於tcp的套接字,關鍵就是兩個迴圈,一個連結迴圈,一個通訊迴圈

    socketserver模組中分兩大類:server類(解決連結問題)和request類(解決通訊問題)

  • socketserver模組的使用

    基於TCP協議的套接字--支援併發(拿之前通訊的例子,主要針對服務端,客戶端不變)
# 服務端.py

import socketserver  # 匯入模組
# 先定義一個類,這個類專門解決通訊迴圈的,必須繼承一個類BaseRequestHandler
class MyRequestHandler(socketserver.BaseRequestHandler): 
    def handle(self):   # 必須要寫一個函式,叫handle的方法,裡面放通訊迴圈
        while True:
            try:
                data = self.request.recv(1024)  # 最大接收的位元組數
                if len(data) == 0:
                    break
                print(data)
                self.request.send(data.upper())
            except Exception:
                break
        self.request.close()

# 連結迴圈,套接字屬於IO密集型,對於IO密集型應該使用多執行緒
# 多執行緒ThreadingTCPServer裡面放:監聽的服務端ip和埠、定義的類、bind_and_activate=True
# bind_and_activate=True等同於bind()並且listen()預設屬性為True無需新增。
server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyRequestHandler,bind_and_activate=True)
# 一直對外提供服務
server.serve_forever()
# serve_forever()每建成一個連結,都呼叫MyRequestHandler這個類,建立一個物件,
# 把它建成的連結物件賦值給self下面的request進行通訊


# 整體邏輯:相當於客戶端每發來一個請求,服務端就啟一個執行緒,每啟一個執行緒就去執行物件下面的
# handle方法,把跟這個客戶端所有相關的套接字資訊全都放到self物件裡面去並觸發這個物件下面
# 的handle方法用這個方法跟客戶端進行通訊


# 客戶端.py (可實現多個客戶端同時通訊)
import socket
# 1、買手機
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 流式協議

# 2、打電話
phone.connect(('127.0.0.1',8080))

# 3、發\收資料
while True:
    msg = input('>>>: ').strip()
    if len(msg) == 0:
        continue
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)
    print(data.decode('utf-8'))

# 4、關閉
phone.close()
基於UDP協議的套接字--支援併發
# 服務端.py

import socketserver

class MyRequesthanlder(socketserver.BaseRequestHandler):
    # 必須要寫一個函式,叫handle的方法,裡面放通訊迴圈
    def handle(self): 
        # 收到訊息,進行解壓。第一個值是客戶端發來的資料。第二個值是套接字物件,用它來回訊息
        data,server = self.request       
        # 將收到的訊息轉大寫回復,所有套接字資訊都封裝進self裡了
        server.sendto(data.upper(),self.client_address)


server = socketserver.ThreadingUDPServer(('127.0.0.1',9999),MyRequesthanlder)

server.serve_forever()

# 整體邏輯同上面TCP協議一樣

# 客戶端.py
from socket import *

client = socket(AF_INET,SOCK_DGRAM)

while True:
    msg = input(">>>>:").strip()
    client.sendto(msg.encode('utf-8'),('127.0.0.1',9999))
    res,server_addr = client.recvfrom(1024)
    print(res.decode('utf-8'))



  • 作業系統

  • 為什麼要有作業系統。

    程式設計師無法把所有的硬體操作細節都瞭解到,管理這些硬體並且加以優化使用是非常繁瑣的工作,這個繁瑣的工作就是作業系統來乾的,有了他,程式設計師就從這些繁瑣的工作中解脫了出來,只需要考慮自己的應用軟體的編寫就可以了,應用軟體直接使用作業系統提供的功能來間接使用硬體。
  • 什麼是作業系統

    作業系統就是一個協調、管理和控制計算機硬體資源和軟體資源的控制程式。

    作業系統位於計算機硬體與應用軟體之間,本質也是一個軟體。作業系統由作業系統的核心(運行於核心態,管理硬體資源)以及系統呼叫(運行於使用者態,為應用程式設計師寫的應用程式提供系統呼叫介面)兩部分組成,所以,單純的說作業系統是運行於核心態的,是不準確的

  • 必備的理論基礎

一 作業系統的作用:
    1:隱藏醜陋複雜的硬體介面,提供良好的抽象介面
    2:管理、排程程序,並且將多個程序對硬體的競爭變得有序

二 多道技術:
    1.產生背景:針對單核,實現併發
    ps:
    現在的主機一般是多核,那麼每個核都會利用多道技術
    有4個cpu,運行於cpu1的某個程式遇到io阻塞,會等到io結束再重新排程,會被排程到4個
    cpu中的任意一個,具體由作業系統排程演算法決定。

    2.空間上的複用:如記憶體中同時有多道程式
    3.時間上的複用:複用一個cpu的時間片
    
強調:遇到io切,佔用cpu時間過長也切,核心在於切之前將程序的狀態儲存下來,這樣
     才能保證下次切換回來時,能基於上次切走的位置繼續執行

  • 程序的相關概念

    程序:程序指的就是程式的執行過程,是一個動態的概念

    程式:程式就是一系列的程式碼檔案,是一個靜態的概念

    程序也可以說成是作業系統幹活的過程,
    一個程序說白了就是作業系統控制硬體來執行應用程式的過程

    所以說程序是作業系統最核心的概念,沒有之一,研究程序就是在研究作業系統

    併發、並行與序列

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

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

    並行:多個任務真正意義上的同時執行,只有具備多個cpu才能實現並行

    序列:一個任務執行完畢後才能開啟下一個任務然後執行

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

    有四個核,六個任務,這樣同一時間有四個任務被執行,假設分別被分配給了cpu1,cpu2,cpu3,

    cpu4

    一旦任務1遇到I/O就被迫中斷執行,此時任務5就拿到cpu1的時間片去執行,這就是單核下的多道技術

    而一旦任務1的I/O結束了,作業系統會重新呼叫它(需知程序的排程、分配給哪個cpu執行,由作業系統說了算),可能被分配給四個cpu中的任意一個去執行

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

    啟動一個程序來防毒(360軟體)

    啟動一個程序來看電影(暴風影音)

    啟動一個程序來聊天(騰訊QQ)

    所有的這些程序都需被管理,於是一個支援多程序的多道程式系統是至關重要的

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

  • 提交任務的兩種方式:

    同步:提交一個任務,提交之後在原地等待提交的結果,之後再去提交下一個任務。所謂同步,就是在發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不會返回。按照這個定義,其實絕大多數函式都是同步呼叫。但是一般而言,我們在說同步、非同步的時候,特指那些需要其他部件協作或者需要一定時間完成的任務。
    非同步:提交一個任務,提交之後不等提交的任務執行完直接提交下一個任務。非同步的概念和同步相對。當一個非同步功能呼叫發出後,呼叫者不能立刻得到結果。當該非同步功能完成後,通過狀態、通知或回撥來通知呼叫者。如果非同步功能用狀態來通知,那麼呼叫者就需要每隔一定時間檢查一次,效率就很低(有些初學多執行緒程式設計的人,總喜歡用一個迴圈去檢查某個變數的值,這其實是一 種很嚴重的錯誤)。如果是使用通知的方式,效率則很高,因為非同步功能幾乎不需要做額外的操作。至於回撥函式,其實和通知沒太多區別。
  • 一個任務執行的三種狀態

    執行態(Running)

    當程序正在被CPU執行,或已經準備就緒隨時可由排程程式執行,則稱該程序為處於執行態

    就緒態(Ready)

    若此時程序沒有被CPU執行,則稱其處於就緒執行狀態。

    阻塞態(Blocked)

    正在執行的程序,由於等待某個事件發生而無法執行時,便處於阻塞狀態。引起程序阻塞的事件可有多種,例如,等待I/O完成、申請緩衝區不能滿足、等待信件(訊號)等

    程序三種狀態間的轉換

    就緒→執行處於就緒狀態的程序,當程序排程程式為之分配任務後,該程序便由就緒狀態轉變成執行狀態。
    執行→就緒處於執行狀態的程序在其執行過程中,因排程程式選擇另一個程序,於是程序從執行狀態轉變成就緒狀態。
    執行→阻塞正在執行的程序因等待某種事件發生而無法繼續執行時,便從執行狀態變成阻塞狀態。
    阻塞→就緒處於阻塞狀態的程序,若其等待的事件已經發生,於是程序由阻塞狀態轉變為就緒狀態。

    阻塞:阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起(如遇到io操作)。函式只有在得到結果之後才會將阻塞的執行緒啟用。有人也許會把阻塞呼叫和同步呼叫等同起來,實際上他是不同的。對於同步呼叫來說,很多時候當前執行緒還是啟用的,只是從邏輯上當前函式沒有返回而已。
    非阻塞(執行、就緒):非阻塞和阻塞的概念相對應,指在不能立刻得到提交結果之前也會立刻返回,同時該函式不會阻塞當前執行緒。

    總結:

    1. 同步與非同步針對的是函式/任務的呼叫方式:同步就是當一個程序發起一個函式(任務)呼叫的時候,一直等到函式(任務)完成,而程序繼續處於啟用狀態。而非同步情況下是當一個程序發起一個函式(任務)呼叫的時候,不會等函式返回,而是繼續往下執行當,函式返回的時候通過狀態、通知、事件等方式通知程序任務完成。

    2. 阻塞與非阻塞針對的是程序或執行緒:阻塞是當請求不能滿足的時候就將程序掛起,而非阻塞則不會阻塞當前程序

  • 開啟程序的兩種方式

    程序是作業系統的概念,程序是由作業系統來開啟的,例如你告訴作業系統有一段程式碼需要執行,把這段程式碼交給作業系統,就是一個程序。應用程式要開啟程序,是為了實現多個任務的併發,真正能開啟程序,管理程序的是作業系統。開啟程序就是在給作業系統發起系統呼叫。呼叫的是作業系統的介面,而介面在Windows系統和Linux系統上是不一樣的(Windows是CreateProcess,Linux是Fork),不同作業系統介面不同很正常,我們只需呼叫封裝好的模組就可以,這個模組解決了跨平臺性問題。

  • multiprocessing模組介紹

    python中的多執行緒無法利用多核優勢,如果想要充分地使用多核CPU的資源(os.cpu_count()檢視),在python中大部分情況需要使用多程序。Python提供了multiprocessing。 multiprocessing模組用來開啟子程序,並在子程序中執行我們定製的任務(比如函式),該模組與多執行緒模組threading的程式設計介面類似。

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

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

    建立程序的類

由該類例項化得到的物件,表示一個子程序中的任務(尚未啟動)
Process([group [, target [, name [, args [, kwargs]]]]])

強調:
1. 需要使用關鍵字的方式來指定引數
2. args指定的為傳給target函式的位置引數,是一個元組形式,必須有逗號

引數介紹:

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

target 表示呼叫物件,即子程序要執行的任務
 
args 表示呼叫物件的位置引數元組,args=(1,2,'egon',)

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

name 為子程序的名稱

方法介紹:

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開啟的程序

屬性介紹:

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字元的字串。這個鍵的用途是為涉及網

Process類的使用

注意:在windows中Process()必須放到# if name == 'main':下

開啟程序的方式一:可以讓多個任務併發起來在後臺同時執行

from multiprocessing import Process

import os
import time


def task(n):
    print('父程序:%s 自己:%s 正在執行' %(os.getppid(),os.getpid()))
    time.sleep(n)
    print('父程序:%s 自己:%s 正在執行' % (os.getppid(), os.getpid()))

if __name__ == '__main__':  # Windows規定啟程序的程式碼必須放在它下面
    # target表示呼叫物件,即子程序要執行的任務 ,args表示呼叫物件的位置引數元組
    p = Process(target=task,args=(3,)) 
    p.start()  # 通知作業系統開啟程序 
    print('主',os.getpid())

開啟程序的方式二:用自定義類的方式(效果同方式一樣)

from multiprocessing import Process

import os
import time

class Myprocess(Process): # 自定義的類必須繼承Process類
    def __init__(self,n):
        super().__init__()
        self.n = n
        
    def run(self)->None:
        print('父程序:%s 自己:%s 正在執行' % (os.getppid(),os.getpid()))
        time.sleep(self.n)
        print('父程序:%s 自己:%s 正在執行' % (os.getppid(), os.getpid()))
        
        
if __name__ == '__main__':
    p = Myprocess(3)
    p.start()
    print('主',os.getpid())
        
  • 程序之間記憶體空間彼此隔離

from multiprocessing import Process
import time

count = 100 # 在windows系統中把全域性變數定義在if __name__ == '__main__'之上就可以了

def task():
    global count
    count = 0

if __name__ == '__main__':
    p = Process(target=task)
    p.start()  # 通知作業系統開啟程序
    time.sleep(5)  # 5秒夠子程序啟動起來執行完
    print("主",count)
    
   
# 執行count = 100產生一個變數,定義一個函式,主程序裡面有一份,然後開了一個子程序
# 開子程序的時候作業系統會把父程序資料一模一樣複製給一份子程序,所以子程序也有100的值,
# 接著子程序啟動的時候會執行task(),task()會將100改為0了,執行之後子程序改成功了,
# 那主程序呢?主程序的100仍然等於100,證明程序之間記憶體空間是彼此隔離的

  • 程序物件的方法

# 例1:
from multiprocessing import Process
import os
import time

count = 100

def task():
    global count
    count = 0

if __name__ == '__main__':
    p = Process(target=task)
    p.start()  # 通知作業系統開啟程序
    p.join()   # 主執行緒等待p終止(強調:是主執行緒處於等的狀態,而p是處於執行的狀態)
    print("主",count)

    
    
# 例2:

from multiprocessing import Process
import os
import time

def task(n):
    print(os.getpid())
    time.sleep(n)

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

    start = time.time()
    p1.start()  # 通知作業系統開啟程序
    p1.join()

    p2.start()  # 通知作業系統開啟程序
    p2.join()

    p3.start()  # 通知作業系統開啟程序
    p3.join()

    stop = time.time()
    print(stop - start)
# 加上join並不是變成串行了,而是看join放在什麼位置,join往後放看到的是併發

# 例3:

from multiprocessing import Process
import os
import time


def task(n):
    print(os.getpid())
    time.sleep(n)


if __name__ == '__main__':
    p1 = Process(target=task, args=(3,),name='程序1')
    p1.start()
    print(p1.name,p1.pid)
    # p1.join()
    p1.terminate()
    time.sleep(0.01)
    print(p1.is_alive())