多進程和多線程


進程的概念

進程:一個正在執行的程序

  • 計算機程序是存儲在磁盤上的可執行二進制(或其他類型)文件,只有把它們加載到內存中,並被操作系統調用,它們才會擁有其自己的生命周期。
  • 進程是表示的一個正在執行的程序。
  • 每個進程都擁有自己的地址空間、內存、數據棧以及其他用於跟蹤執行的輔助數據。
  • 操作系統負責其上所有進程的執行,並為這些進程合理地分配執行時間。
  • 進程之間是獨立的,不能共享彼此的數據。

並行與並發

並行:同時進行多個任務
並發:短時間內,運行多個任務

並發:單CPU,多進程並發

     無論是並行還是並發,在用戶看來都是'同時'運行的,不管是進程還是線程,都只是一個任務而已,真實干活的是cpu,cpu來做這些任務,而一個cpu同一時刻只能執行一個任務

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

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

 

並行滿足的條件是總進程數量不多於 CPU核心數量!因此,現在PC運行的程序大部分都是輪詢調度產生的並行假象

舉例:比如PC有四個核,六個任務,這樣同一時間有四個任務被執行,假設分別被分配給了cpu1,cpu2,cpu3,cpu4,

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

   而一旦任務2的I/O結束了,操作系統會重新調用它(需知進程的調度、分配給哪個cpu運行,由操作系統說了算),可能被分配給四個cpu中的任意一個去執行

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

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

 

例子:在python中執行耗時操作

 結果:

用進程來分擔耗時任務后:

python進程使用流程

 

 

線程的概念

  • 線程被稱作輕量級進程。
  • 與進程類似,不過它們是在同一個進程下執行的。
  • 並且它們會共享相同的上下文。
  • 當其他線程運行時,它可以被搶占(中斷)和臨時掛起(也稱為睡眠)
  • 線程的輪訓調度機制類似於進程的輪詢調度,只不過這個調度不是由操作系統來負責,而是由Python解釋器來負責。

GIL鎖

Python在設計的時候,還沒有多核處理器的概念。因此,為了設計方便與線程安全,直接設計了一個鎖。這個鎖要求,任何進程中,一次只能有一個線程在執行。
因此,並不能為多個線程分配多個CPU。所以Python中的線程只能實現並發,而不能實現真正的並行。但是Python3中的GIL鎖有一個很棒的設計,在遇到阻塞(不是耗時)的時候,會自動切換線程。

Scrapy、Django、Flask、Web2py ...等框架 都是使用多線程來完成
GIL鎖帶給我們的新認知,遇到阻塞就自動切換。因此我們可以利用這種機制來有效的避開阻塞 ,充分利用CPU

例子:使用線程來避開阻塞任務

python線程使用流程

 

計算密集型和IO密集型

IO密集型遇到等待時,不消耗CPU,CPU會調度其他程序執行,使用多線程可以有效的進行並發(爬蟲的請求是IO密集型),線程在系統中所占資源較少。
計算密集型:CPU會一直計算,此時使用多線程,反而會耗時更多,此時使用多進程(CPU越多,性能越好)。

使用多進程/多線程實現並發服務器:

import socket
from multiprocessing import Process
from threading import Thread


def read(conn,addr):
    while True:
        data = conn.recv(1024)
        if data:
            print('客戶端{}的消息:{}'.format(addr,data.decode()))
            conn.send(data)
        else:
            conn.close() 
            print('客戶端{}斷開連接'.format(addr))
            break


if __name__ == '__main__':
    server = socket.socket()
    server.bind(('', 9998)) # 注意這些代碼要寫在if...main...判斷下面,因為在Windows中,使用進程的模式類似導入,在判斷外面的代碼會在子進程執行
    server.listen(5)
    while True:
        print('等待客戶端連接')
        conn,addr = server.accept()  # 有客戶端連接,就往下進行
        print('客戶端{}已連接'.format(addr))
        # p = Process(target=read,args=(conn,addr))  # 使用多進程,每來一個客戶端連接,就分出一個進程去和它一對一處理
        # p.start()
        t = Thread(target=read,args=(conn,addr))  # 使用多線程,每來一個客戶端連接,就分出一個線程去和它一對一處理
        t.start()

補充

fork介紹

Unix/Linux操作系統提供了一個fork()系統調用,它非常特殊。普通的函數調用,調用一次,返回一次,但是fork()調用一次,返回兩次,因為操作系統自動把當前進程(稱為父進程)復制了一份(稱為子進程),然后,分別在父進程和子進程內返回。

子進程永遠返回0,而父進程返回子進程的ID。這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調用getppid()就可以拿到父進程的ID。

import os

if __name__ == '__main__':

    print('進程 (%s) start...' % os.getpid())
    pid = os.fork()
    # time.sleep(10)
    if pid == 0:  # 若fork()返回0為子進程
        print("子進程{},父進程{}".format(os.getpid(), os.getppid())) #getpid()獲取當前進程的pid,getppid()獲取父進程的pid
    else:  # 父進程fork返回子進程的id
        print("父進程{},子進程{}".format(os.getpid(), pid))

>>
進程 (3130) start...
父進程3130,子進程3131
子進程3131,父進程3130

multiprocessing模塊介紹

python中的多線程無法利用CPU資源,在python中大部分計算密集型任務使用多進程。如果想要充分地使用多核CPU的資源(os.cpu_count()查看)

python中提供了非常好的多進程包multiprocessing。

multiprocessing模塊用來開啟子進程,並在子進程中執行功能(函數),該模塊與多線程模塊threading的編程接口類似。

multiprocessing的功能眾多:支持子進程、通信和共享數據、執行不同形式的同步,提供了Process、Queue、Pipe、Lock等組件。

 計算密集型任務:如金融分析,科學計算

multiprocessing.current_process() :在任何一個進程中搞清楚自己是誰,在線程中也有類似方法

Process類的介紹

1.創建進程的類

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

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

2.參數介紹

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

target表示調用對象,即子進程要執行的任務

args表示調用對象的位置參數元組,args=(1,2,'al',)

kwargs表示調用對象的字典,kwargs={'name':'al','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,線程中是t.ident 注意只有進程或線程已經啟動才會分配pid或ident

p.exitcode:進程在運行時為None、如果為–N,表示被信號N結束(了解即可)

p.authkey:進程的身份驗證鍵,默認是由os.urandom()隨機生成的32字符的字符串。這個鍵的用途是為涉及網絡連接的底層進程間通信提供安全性,
這類連接只有在具有相同的身份驗證鍵時才能成功(了解即可)

以用面向對象的形式使用進程與線程

關鍵點:

  • 繼承Process或Thread類
  • 重寫__init__方法
  • 重寫run方法
from multiprocessing import Process
import time
import random
import os
class pro(Process): #繼承Process類 def __init__(self,name): super().__init__() # 重寫__init__方法 這里是直接調用父類的初始化方法 self.name = name
def run(self): # 重寫run方法 print('%s is working,父進程%s,當前進程%s'%(self.name,os.getppid(),os.getpid())) time.sleep(1) print('%s end'%self.name)

if __name__ =='__main__': p1 = pro('a') p2 = pro('b') p3 = pro('c') p1.start() p2.start() p3.start() print('主進程',os.getpid()) >> 主進程 9116 b is working,父進程9116,當前進程4316 a is working,父進程9116,當前進程6664 c is working,父進程9116,當前進程10132 b end a end c end

 總結:

Windows用的導入(所以if...main...判斷外面的代碼,在子進程仍會執行)

方法:
p = Process(target=func,name='p1',daemon=True) #實例的時候可以指定方法、名字、是否守護進程
p.current_process() 獲取當前進程對象
p.is_alive() 判斷進程實例是否在運行 是返回True 否返回False
p.terminate() 結束進程 線程無此方法
p.name 獲取進程名字
p.join() 等待子進程結束再往下執行
p.daemon = True 把進程設為守護進程 (父進程結束,守護進程也結束)
注意:如果設置了join() 那么terminate和daemon 就不管用啦
p.pid 獲取當前進程的pid  線程是t.ident  
注意:只有進程(或線程)啟動之后,操作系統(或python解釋器)才會分配pid(或ident)

默認在子進程當中,會關閉標准輸入
import sys
import os
sys.stdin = os.fdopen(0) #加上這行代碼,子進程才能使用標准輸入

多線程的方法與屬性
t.setDaemon(True)
t.setName('p1')
t.getName()

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM