併發程式設計概念大總結--乾貨

愛文飛翔發表於2019-08-01

程式

​ 程式是計算機中最小的資源分配單位,進行中的一個程式就是一個程式。

程式需要作業系統來排程,每個程式執行起來的時候需要給分配一些記憶體,開啟關閉切換時間開銷大,程式之間資料隔離,程式也有資料不安全的問題 用Lock解決

程式的三狀態圖: 就緒 執行 阻塞
就緒-->操系統排程 -->執行-遇到io操作->阻塞-阻塞狀態結束->就緒
                      -時間片到了->就緒

併發程式設計概念大總結--乾貨

程式的排程演算法:給所有的程式分配資源或者分配CPU使用權的一種方法。

  • 短作業優先、先來先服務、多級反饋演算法
  • 多級反饋演算法:
    • 多個任務佇列,優先順序從高到低
    • 新來的任務總是優先順序最高
    • 每個新任務幾乎會立即獲得一個時間片時間
    • 執行完一個時間片之後就會降到下一級佇列中
    • 總是優先順序高的任務都執行完才執行優先順序低的佇列
    • 並且優先順序越高時間片越短

程式開啟和關閉
父程式 開啟了子程式
父程式 負責給 子程式回收子程式結束之後的資源

from multiprocessing import Process
import os

def func():
   print(os.getpid(), os.getppid()) # pid process 子程式id    ppid 父程式id
if __name__ == '__main__':  # 只會在主程式中執行的所有的程式碼,寫在__main__下邊
   print('main:', os.getpid(), os.getppid())
   p = Process(target=func)  # target表示呼叫物件,即子程式要執行的任務
    p = Process(target=func,args=('安文',))
    # p = Process(target=func,kwargs={'name':'安文'})  兩種傳參方式
   p.start()  #非同步非阻塞 啟動程式,並呼叫該子程式中的p.run()

注意:在windows中Process()必須放到# if __name__ == '__main__':下
由於Windows沒有fork,多處理模組啟動一個新的Python程式並匯入呼叫模組。 
如果在匯入時呼叫Process(),那麼這將啟動無限繼承的新程式(或直到機器耗盡資源)。 
這是隱藏對Process()內部呼叫的原,使用if __name__ == “__main __”,這個if語句中的語句將不會在匯入時被呼叫。

開啟程式的另一種方法:

  • 物件導向的方法,通過繼承和重寫run方法完成啟動子程式
from multiprocessing import Process
# class 類名(Process):
#  def __init__(self,引數):
#     self.屬性名=引數
#     super().__init__()
#  def run(self):
#     print("子程式要執行的程式碼")
# p=類名()
# p.start()
Process類的一些其他方法和屬性:name  pid ident  daemon  terminate()  isalive()
p.name   給子程式起名字
is_alive()方法由執行緒呼叫,有返回值,如果執行緒還存活,返回true,如果執行緒消亡,返回false
terminate() #強制結束子程式 非同步非阻塞
守護程式:
# 主程式會等待所有子程式結束,是為了回收子程式的資源
# 守護程式會等待主程式的程式碼執行結束之後再結束,而不是等待整個主程式結束
# 主程式的程式碼什麼時候結束,守護程式就什麼時候結束,和其他子程式的執行進度無關

主程式建立守護程式:

- 守護程式會在主程式程式碼執行結束後就終止
- 守護程式內無法再開啟子程式,否則丟擲異常

注意:程式之間是相互獨立的,主程式程式碼執行結束,守護程式隨即終止
在start一個程式之前設定daemon=True,守護程式會等待主程式的程式碼結束就立即結束
p = Process(target=son2)
p.daemon = True  # 一定要在p.start()前設定,表示設定p是一個守護程式
p.start()
為什麼守護程式只守護主程式的程式碼?而不是等主程式結束之後才結束
#        為了給守護程式回收資源
#     守護程式會等其他子程式結束嗎?不會
一般情況下,多個程式執行順序可能是:
  • 主程式程式碼結束-->守護程式結束-->子程式結束-->主程式結束
  • 子程式結束-->主程式程式碼結束 -->守護程式結束-->主程式結束
程式之間通訊(IPC):
  • 基於檔案:同一臺機器上的多個程式之間通訊
    Queue佇列:基於socket的檔案級別的通訊來完成資料傳遞的,

    ​ 佇列:安全 管道:不安全

  • 基於網路:同一臺機器或者多臺機器上的多程式之間的通訊
    第三方工具:(訊息中介軟體):memcache/redis/rabbitmq/kafka

程式之間可以通過Manager類實現資料共享

共享資料不安全,需要自己加鎖解決資料安全問題

生產者模型 消費者模型:
  • 本質:讓生產資料和消費資料的效率達到平衡並且最大化效率

消費者:通常取到資料之後還要進行某些操作 消費者如何結束:None
生產者:通常在放資料之前需要先通過某些程式碼來獲取資料

# 把原本獲取資料處理資料的完整過程進行了解耦
# 把生產資料和消費資料分開,根據生產和消費的效率不同,
# 來規劃生產者和消費者的個數,讓程式的執行效率達到平衡

#  如果你寫了一個程式所有的程式碼、和功能都放在一起
# 不分函式不分類也不分檔案,就叫這個程式是緊耦合的程式
# 緊耦合程式:程式碼只寫一次,不需要重構
# 鬆耦合的程式:需要重構,不斷迭代 複用程式碼

# 拆分的很清楚的程式 叫做 鬆耦合的程式,鬆耦合程式好
from multiprocessing import Queue,Pipe
# 佇列:ipc程式之間通訊,佇列資料安全,不需要自己加鎖。佇列做通訊,資訊之間傳遞,
# 基於socket實現的,pickle實現,鎖實現,
# pipe管道:也像佇列一樣,可以放資料可以取資料,沒有鎖資料不安全,
# 基於socket、pickle 實現的。沒有鎖 資料不安全
鎖----multiprocessing.Lock:

加鎖可以保證多個程式修改同一塊資料時,同一時間只能有一個任務可以進行修改,即序列修改,導致速度慢了,但保證了資料安全。

鎖:保證資料安全,會降低程式的執行效率,資料安全

互斥鎖,多程式中共享的資料需要加鎖

# from multiprocessing import Lock  #互斥鎖,多程式中共享的資料需要加鎖
# lock=Lock()
# lock.acquire()
# '''被鎖的內容在這裡寫'''
# lock.release()
# with lock:
#  ...

並行:

好,高效,多個cpu在自己的cpu上執行多個程式

併發:

一個cpu多個程式輪流執行,10個程式輪流使用一個cpu

多個程式同時執行,只要一個CPU,多個程式輪流在一個CPU上執行

巨集觀上:多個程式同時執行

微觀上:多個程式輪流在一個CPU上執行,本質上是序列

同步非同步阻塞非阻塞

同步:呼叫一個操作要等待結果
在做A件事的時候發起B事,必須等待B事件結束之後才能繼續A事件
非同步:更快,呼叫一個操作,不等待結果
在做A件事的時候發起B事,不需要等待B事件結束之後才能繼續A事件

阻塞:如果CPU不工作 input accept recv sleep connect
非阻塞:如果CPU在工作

# 同步阻塞:呼叫一個函式需要等待這個函式的執行結果,並且在執行這個函式的過程中CPU不工作 num=input('>>>')
# 同步非阻塞:呼叫一個函式需要等待這個函式的執行結果,並且在執行這個函式的過程中CPU工作 ret=eval(1+2+3+4)
# 非同步非阻塞:start() 呼叫一個函式不需要等待這個函式的執行結果,並且在執行這個函式的過程中CPU工作
# 非同步阻塞:呼叫一個函式不需要等待這個函式的執行結果,並且在執行這個函式的過程中CPU不工作
#  10個非同步的程式,獲取這個程式的返回值,並且能做到哪一個程式結束,就先獲取誰的返回值
# 同步阻塞
# 呼叫函式必須等待結果,cpu沒工作,input sleep recv accept content get
# 同步非阻塞 *******
# 呼叫函式必須等待結果,cpu工作了,呼叫了一個高計算的函式 ,strip,eval,max,sorted同步非阻塞
# 非同步阻塞
# 呼叫函式不需要立即獲取結果,而是繼續做其他的事情,在獲取結果的時候不知道先獲取誰的,但是總之要等
# 非同步非阻塞 *******
# 呼叫函式不需要立即獲取結果,也不需要等 start  terminate

執行緒

執行緒是計算機中能被CPU排程的最小單位,執行緒是程式中的一個單位,不能脫離程式存在,執行緒必須存在程式內。cpu執行的是解釋之後的執行緒中的程式碼,同一個程式中的多個執行緒可以同時被CPU執行。執行緒之間的資料是共享的,作業系統排程的最小單位,可以利用多核,作業系統排程,資料不安全,開啟關閉切換時間開銷非常小

全域性直譯器 GIL (global interpreter lock):

  • cpython直譯器下有個GIL鎖全域性直譯器鎖);全域性直譯器鎖的出現主要是為了完成GC的回收機制,對不同執行緒的引用計數的變化記錄的更加精準; 導致了同一個程式只能有一個執行緒真正被CPU執行,導致了同一個程式中的多個執行緒不能利用多核(不能並行)
  • 節省的是IO操作的時間,而不是CPU計算的時間,因為CPU的計算速度非常快,大部分情況下,我們沒辦法把一條程式中所有的io操作都規避掉

GC:垃圾回收機制,就是一個執行緒

pypy直譯器 gc不能用多核

jpython直譯器 gc能利用多核

開啟執行緒:
# import time
# from threading import Thread
# def func(i):
#  print("start%s"%i)
#  time.sleep(1)
#  print("end%s"%i)
# for i in range(10):
#  Thread(target=func,args=(i,)).start()

# 物件導向方式起執行緒
# from threading import Thread
# class MyThread(Thread):
#   def __init__(self,a,b):
#       self.a=a
#       self.b=b
#       super().__init__()
#   def run(self):
#       print(self.ident)
# t=MyThread(1,2)
# t.start()   #開啟執行緒 才線上程中執行run方法
# print(t.ident)
# 執行緒是不能從外部關閉的 沒有terminate
# 所有的子執行緒只能是自己執行完程式碼之後就關閉
# current_thread()  當前執行緒的物件,current_thread().itent()執行緒的id
# enumerate() 列表  儲存了所有活著的執行緒物件,包括主執行緒和子執行緒
# active_count() 數字  儲存了所有活著的執行緒個數

主執行緒會等子執行緒結束之後才結束,子執行緒不結束,主執行緒就不結束

;因為主執行緒結束程式就會結束

守護執行緒 : 守護執行緒隨著主執行緒的結束而結束;
守護執行緒會在主執行緒的程式碼結束之後繼續守護其他子執行緒
# 守護程式  會隨著主程式的程式碼結束而結束,
# 如果主程式程式碼結束之後還有其他子程式在執行,守護程式不守護
# 守護執行緒  會隨著主執行緒的結束而結束
# 如果主執行緒程式碼結束之後還有其他子執行緒在執行,守護執行緒也守護

# 守護程式和守護執行緒的結束原理不同
# 守護程式需要主程式來回收、守護執行緒是隨著程式的結束才結束;所有的執行緒都會隨著程式的結束而被回收的
# 其他子執行緒-->主執行緒結束-->主程式結束-->整個程式中所有的資源都被回收-->守護執行緒也會被回收

執行緒之間資料不安全
# += -= *= /= while if 資料不安全 +和賦值是分開的兩個操作
# append pop strip資料安全
# 列表中的方法或者字典中的方法去操作全域性變數的時候 資料安全
執行緒鎖:
單例模式加鎖 天生執行緒安全
import time
class A:
   from threading import Lock
   __instance=None
   lock=Lock()
   def __new__(cls, *args, **kwargs):
      with cls.lock:
         if not cls.__instance:
            time.sleep(0.000001)
            cls.__instance=super().__new__(cls)
      return cls.__instance
def func():
   a=A()
   print(a)
from threading import Thread
for i in range(10):
   Thread(target=func).start()
互斥鎖和遞迴鎖的區別:
  • 遞迴鎖 :(RLock)效率低,但是解決死鎖現象有奇效 萬能鑰匙,臨時解決一些死鎖現象
  • 遞迴鎖:在同一程式中可以被acquire多次,但一次acquire必須對應一次release
  • 互斥鎖 :效率高,能夠處理多個執行緒之間資料安全,但是多把互斥鎖交替的使用容易產生死鎖現象
  • 互斥鎖:在同一個程式中不能被連續acquire多次,一次acquire對應一次release
死鎖現象是怎麼產生的?
多把(互斥鎖/遞迴)鎖並且在多個執行緒中交叉使用,(比如:兩把鎖,在第一把鎖沒有釋放之前就獲取第二把鎖)
解決死鎖現象:
  • ​ 出現了死鎖現象,最快速的解決方案把所有的互斥鎖都改成一把遞迴鎖,程式的效率會降低

佇列:queue;

執行緒佇列特點:資料安全,一定是加鎖了,先進先出

什麼是池?
  • 要在程式還沒開始的時候,還沒提交任務先建立幾個執行緒或程式放在一個池子裡
為什麼要用池?
  • 如果先開好執行緒或者程式,那麼有任務之後就可以直接使用這個池中的資料了,節省時間
  • 並且開好的執行緒或程式一直存在池中,可以被多個任務反覆利用,這樣極大的減少了開啟和關閉、排程執行緒/程式的時間開銷。
  • 池中的執行緒/程式個數控制了作業系統需要排程的任務個數,控制池中的單位有利於提高作業系統的效率,減輕作業系統的負擔
# multiprocessing 模組 仿照threading寫的pool
# concurrent.futures模組,執行緒池和程式池都能夠用相似的方式開啟和使用
#ThreadPoolExecutor:執行緒池,提供非同步呼叫
#ProcessPoolExecutor: 程式池,提供非同步呼叫
執行緒池:(一般根據io的比例定製)
import time
import random
from threading import current_thread
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def func(a,b):
    print(current_thread().ident,'start',a,b)   #接收引數
    time.sleep(random.randint(1,4))
    print(current_thread().ident,'end')
tp=ThreadPoolExecutor(4)
for i in range(20):
    tp.submit(func,i,i+1)   #按位置傳引數
    tp.submit(func, a=i, b=i + 1)   #關鍵字傳引數
程式池:
程式池(高計算場景,沒有io(沒有檔案操作、沒有資料庫操作、沒有網路操作
、沒有input))
import os
import time
import random
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def func(a,b):
   print(os.getpid(),'start',a,b)   #接收引數
   time.sleep(random.randint(1,4))
   print(os.getpid(),'end')
if __name__ == '__main__':
   pp=ProcessPoolExecutor(4)
   for i in range(20):
      pp.submit(func,i,i+1)   #按位置傳引數
      pp.submit(func, a=i, b=i + 1)   #關鍵字傳引數
回撥函式:效率最高
import time
import random
from threading import current_thread
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def func(a,b):
   print(current_thread().ident,'start',a,b)   #接收引數
   time.sleep(random.randint(1,4))
   print(current_thread().ident,'end',a)
   return (a,a*b)
def print_func(ret):    #非同步阻塞
   print(ret.result())
if __name__ == '__main__':
   tp=ThreadPoolExecutor(4)
   for i in range(20):     #提交任務是非同步非阻塞
      ret=tp.submit(func,i,i+1)   #按位置傳引數
      ret.add_done_callback(print_func)   #非同步阻塞

# 回撥函式 給ret物件繫結一個回撥函式,等待ret對應的任務有了結果之後立即呼叫print_func這個函式
# 就可以對結果立即進行處理,而不用按照順序接收處理結果

協程

協程:是作業系統不可見的
  • 協程本質就是一條執行緒,多個任務在一條執行緒上來回切換;
  • 利用協程這個概念實現的內容:規避io操作,達到將一條執行緒中的io操作降到最低的目的
# 切換並規避io操作的模組:
# gevent:利用了greenlet底層模組完成的切換+自動規避io的功能
# asyncio:利用了yield底層語法完成的切換+自動規避io的功能
# tornado非同步的web框架
# yield from:為了更好的實現協程
#  send為了更好的實現協程
# asyncio 模組 基於Python原生的協程的概念正式被成立
# 特殊的在Python中提供協程功能的關鍵字:aysnc  await
使用者級別的協程還有什麼好處:
  • 減輕作業系統的負擔
  • 一條執行緒如果開了多個協程,那麼給作業系統的印象是執行緒很忙,
  • 這樣能爭取一些時間片時間來被cpu執行,程式的效率就提高了
協程:
import gevent
def func():
   print('start func')
   gevent.sleep(1)
   print('end func')
g=gevent.spawn(func)
g1=gevent.spawn(func)
gevent.joinall([g,g1])

asyncio:

import asyncio
async def func(name):   #async協程函式
   print("start",name)
   #await 關鍵字必須寫在async函式裡
   await asyncio.sleep(1)  #await後面可能會生成阻塞的方法
   print('end')
loop=asyncio.get_event_loop()  #事件迴圈
loop.run_until_complete(asyncio.wait([func('wudi'),func('anwen')]))

程式、執行緒和協程

# 程式:程式之間資料隔離,資料不安全,由作業系統(級別)切換,開銷非常大,能利用多核
# 執行緒:執行緒之間資料共享,資料不安全,由作業系統(級別)切換,開銷小,不能利用多核
# 協程:協程之間資料共享,資料安全  ,使用者級別,開銷更小,不能利用多核,協程的所有切換都基於使用者,那麼只有在使用者級別
# 能夠感知到的io操作才會用協程模組來切換來規避(socket,請求網頁的)
asyncio
async def func(name):   #async協程函式
   print("start",name)
   #await 關鍵字必須寫在async函式裡
   await asyncio.sleep(1)  #await後面可能會生成阻塞的方法
   print('end')
loop=asyncio.get_event_loop()  #事件迴圈
loop.run_until_complete(asyncio.wait([func('wudi'),func('anwen')]))

程式、執行緒和協程

# 程式:程式之間資料隔離,資料不安全,由作業系統(級別)切換,開銷非常大,能利用多核
# 執行緒:執行緒之間資料共享,資料不安全,由作業系統(級別)切換,開銷小,不能利用多核
# 協程:協程之間資料共享,資料安全  ,使用者級別,開銷更小,不能利用多核,協程的所有切換都基於使用者,那麼只有在使用者級別
# 能夠感知到的io操作才會用協程模組來切換來規避(socket,請求網頁的)

相關文章