Python中的多任務(線程、進程、協程)

1. 多任務的概念

多任務簡單地說,就是操做系統能夠同時運行多個任務。分爲並行和併發兩種。python

1.1 並行

指的是任務數小於等於CPU核數,即任務真的是一塊兒執行的算法

1.2 併發

指的是任務數多於CPU核數,經過操做系統的各類任務調度算法,實現用多個任務一塊兒執行(實際上總有一些任務不在執行,由於切換任務的速度至關快,看上去一塊兒執行而已)瀏覽器

1.3 網絡的概念

網絡就是輔助雙方或多方,鏈接在一塊兒的輔助工具服務器

1.4 網絡的做用

網絡可以實現數據的相互傳送,可以通訊,互相傳遞信息等。網絡

1.5 ip地址

ip地址就是在網絡中標記一臺計算機的地址,好比192.168.1.1;在本地局域網上是惟一的。多線程

  1. 在這麼多網絡IP中,國際規定有一部分IP地址是用於咱們的局域網使用,也就

是屬於私網IP,不在公網中使用的,它們的範圍是:併發

10.0.0.0~10.255.255.255

172.16.0.0~172.31.255.255

192.168.0.0~192.168.255.255
複製代碼
  1. 注意

IP地址127.0.0.1~127.255.255.255用於迴路測試,app

如:127.0.0.1能夠表明本機IP地址,用http://127.0.0.1就能夠測試本機中配置的Web服務器。socket

1.6 端口(port)

端口號就是電腦上某個程序對應的地址,用來標記某個程序,端口號只有整數,範圍是從0到65535async

  • 知名端口:知名端口是衆所周知的端口號,範圍從0到1023,都已被佔用
  • 動態端口:動態端口的範圍是從1024到65535,之因此稱爲動態端口,是由於它通常不固定分配某種服務,而是動態分配。

1.5 socket

socket是一種進程之間的通訊方式,簡稱套接字。它能實現不一樣主機間的進程間通訊,咱們網絡上各類各樣的服務大多都是基於 Socket 來完成通訊的

例如咱們天天瀏覽網頁、QQ 聊天、收發 email 等等。

  • 建立socket
import socket
socket.socket(AddressFamily, Type)
複製代碼

說明:

函數 socket.socket 建立一個 socket,該函數帶有兩個參數:

Address Family:能夠選擇 AF_INET(用於 Internet 進程間通訊) 或者 AF_UNIX(用於同一臺機器進程間通訊),實際工做中經常使用AF_INET Type:套接字類型,能夠是 SOCK_STREAM(流式套接字,主要用於 TCP 協議)或者 SOCK_DGRAM(數據報套接字,主要用於 UDP 協議)

2. 線程

python的thread模塊是比較底層的模塊,python的threading模塊是對thread作了一些包裝的,能夠更加方便的被使用

2.1 多線程執行

#coding=utf-8
import threading
import time

def saySorry():
    print("我能吃飯了嗎?")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=saySorry)
        t.start() #啓動線程,即讓線程開始執行
複製代碼
  • 使用的是多線程併發的操做,當調用start()時,纔會真正的建立線程,而且開始執行
  • 主線程結束後,子進程當即結束

2.2 線程執行代碼的封裝

python的threading.Thread類有一個run方法,用於定義線程的功能函數,能夠在本身的線程類中覆蓋該方法。而建立本身的線程實例後,經過Thread類的start方法,能夠啓動該線程,交給python虛擬機進行調度,當該線程得到執行的機會時,就會調用run方法執行線程。

2.3 線程的執行順序

多線程程序的執行順序是不肯定的。當執行到sleep語句時,線程將被阻塞,到sleep結束後,線程進入就緒狀態,等待調度。而線程調度將自行選擇一個線程執行。

2.4 多線程共享全局變量

  • 在一個進程內的全部線程共享全局變量,很方便在多個線程間共享數據
  • 缺點就是,線程是對全局變量隨意更改可能形成多線程之間對全局變量的混亂
  • 若是多個線程同時對同一個全局變量操做,會出現資源競爭問題,從而數據結果會不正確

2.5 互斥鎖

當多個線程幾乎同時修改某個共享數據的時候,須要進行同步控制。

某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲「鎖定」,其餘線程不能更改;直到該線程釋放資源,將資源的狀態變成「非鎖定」,其餘的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操做,從而保證了多線程狀況下數據的正確性。

threading模塊中定義了Lock類,能夠方便的處理鎖定:

# 建立鎖
mutex = threading.Lock()

# 鎖定
mutex.acquire()

# 釋放
mutex.release()
複製代碼
  • 若是這個鎖以前沒有上鎖的,那麼acquire不會堵塞
  • 若是在調用acquire對這個鎖上鎖以前它已經被其餘線程上了鎖,那麼此時acquire會堵塞,直到這個鎖被解鎖爲止

2.6 死鎖

在線程鍵共享多個資源的時候,若是兩個線程分別佔有一部分資源而且同時等待對方的資源,就會形成死鎖。

能夠添加超時時間等,解決死鎖

案例:多線程版udp聊天器

import socket
import threading


def send_msg(udp_socket):
    """獲取鍵盤數據,並將其發送給對方"""
    while True:
        # 1. 從鍵盤輸入數據
        msg = input("\n請輸入要發送的數據:")
        # 2. 輸入對方的ip地址
        dest_ip = input("\n請輸入對方的ip地址:")
        # 3. 輸入對方的port
        dest_port = int(input("\n請輸入對方的port:"))
        # 4. 發送數據
        udp_socket.sendto(msg.encode("utf-8"), (dest_ip, dest_port))


def recv_msg(udp_socket):
    """接收數據並顯示"""
    while True:
        # 1. 接收數據
        recv_msg = udp_socket.recvfrom(1024)
        # 2. 解碼
        recv_ip = recv_msg[1]
        recv_msg = recv_msg[0].decode("utf-8")
        # 3. 顯示接收到的數據
        print(">>>%s:%s" % (str(recv_ip), recv_msg))


def main():
    # 1. 建立套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 2. 綁定本地信息
    udp_socket.bind(("", 7890))

    # 3. 建立一個子線程用來接收數據
    t = threading.Thread(target=recv_msg, args=(udp_socket,))
    t.start()
    # 4. 讓主線程用來檢測鍵盤數據而且發送
    send_msg(udp_socket)

if __name__ == "__main__":
    main()
複製代碼

3. 進程

multiprocessing模塊就是跨平臺版本的多進程模塊,提供了一個Process類來表明一個進程對象,這個對象能夠理解爲是一個獨立的進程,能夠執行另外的事情。

建立子進程時,只須要傳入一個執行函數和函數的參數,建立一個Process實例,用start()方法啓動

# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time

def run_proc():
    """子進程要執行的代碼"""
    print('子進程運行中,pid=%d...' % os.getpid())  # os.getpid獲取當前進程的進程號
    print('子進程將要結束...')

if __name__ == '__main__':
    print('父進程pid: %d' % os.getpid())  # os.getpid獲取當前進程的進程號
    p = Process(target=run_proc)
    p.start()
複製代碼

3.1 process語法結構以下:

Process([group [, target [, name [, args [, kwargs]]]]])

  • target:若是傳遞了函數的引用,能夠任務這個子進程就執行這裏的代碼

  • args:給target指定的函數傳遞的參數,以元組的方式傳遞

  • kwargs:給target指定的函數傳遞命名參數

  • name:給進程設定一個名字,能夠不設定

  • group:指定進程組,大多數狀況下用不到 Process建立的實例對象的經常使用方法:

  • start():啓動子進程實例(建立子進程)

  • is_alive():判斷進程子進程是否還在活着

  • join([timeout]):是否等待子進程執行結束,或等待多少秒

  • terminate():無論任務是否完成,當即終止子進程

  • Process建立的實例對象的經常使用屬性:

  • name:當前進程的別名,默認爲Process-N,N爲從1開始遞增的整數

  • pid:當前進程的pid(進程號)

進程之間不共享全局變量

3.2 進程與線程的區別

  • 進程是系統進行資源分配和調度的一個獨立單位
  • 線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。線程本身基本不擁有系統資源,只擁有一點在運行中必不可少的資源,可是它可與同屬一個進程的其它線程共享進程所擁有的所有資源
  • 一個程序至少有一個進程,一個進程至少有一個線程
  • 線程的劃分尺度小於進程,使得多線程序的併發性高。
  • 進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率
  • 線程不能獨立執行,必須依存在進程中
  • 能夠將進程理解爲工廠中的一條流水線,而其中的線程就是這個流水線上的工人
  • 線程和進程在使用上各有優缺點:線程執行開銷小。但不利於資源的管理和保護,而進程正相反。

3. 進程池Pool

當須要建立的子進程數量巨大時,就能夠用到multiprocessing模塊提供的Pool方法

  • apply_async(func[,args[,kwds]]):使用非阻塞方式調用func,(並行執行,堵塞方式必須等待上一個進程退出才能執行下一個進程),args爲傳遞給func的參數列表,kwds爲傳遞給func的關鍵字參數列表
  • close():關閉Pool,使其再也不接收新的任務
  • terminate():無論任務是否完成,當即終止
  • join():主進程阻塞,等待子進程的退出,必須在close或terminate以後使用
  • 在程序執行過程當中,關閉進程池,則程序會當即中止,不會再繼續執行後續語句。

4. Queue

進程是經過導入Queue,用Queue實現多進程之間的數據傳遞,Queue自己是一個消息隊列程序。

  • Queue.qsize():返回當前隊列包含的消息數量
  • Queue.empty():若是隊列爲空,返回True,反之False
  • Queue.full():若是隊列滿了,返回True,反之False
  • Queue.get([block[,timeout]]):獲取隊列中的一條消息,而後將其從隊列中移除,block默認值爲True
  • Queue.put_nowait(item):至關Queue.put(item,False)

5. 迭代器

5.1 可迭代對象

  1. 咱們把能夠經過for...in...這類語句迭代讀取一條數據供咱們使用的對象稱之爲可迭代對象(Iterable)**
  2. 可迭代對象的本質就是能夠向咱們提供一個這樣的中間"人"即迭代器幫助咱們對其進行迭代遍歷使用。
  3. 可迭代對象經過__iter__方法向咱們提供一個迭代器,咱們在迭代一個可迭代對象的時候,實際上就是先獲取該對象提供的一個迭代器,而後經過這個迭代器來依次獲取對象中的每個數據,也就是說,一個具有了__iter__方法的對象,就是一個可迭代對象。

5.2 如何判斷一個對象是否能夠迭代

  • 可使用isinstance()判斷一個對象是不是iterable對象
  • 生成器是一類特殊的迭代器

5.3 建立生成器的方法

  1. 只要把一個列表生成式的[]改爲()
  2. 只要在def中有yield關鍵字的就稱爲生成器,使用了yield關鍵字的函數再也不是函數,而是生成器

5.4 喚醒

除了可使用next()函數來喚醒生成器繼續執行外,還可使用send()函數來喚醒來執行,使用send()函數的一個好處是能夠在喚醒的同時向斷點處傳入一個附加數據

6. gevent

6.1 geven使用

import gevent
def f(n):
    for i in range(n):
        print(gevent.getcurrent(),i)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
複製代碼

運行結果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4
複製代碼

3個greenlet是依次運行而不是交替運行,能夠導入time,添加延時切換執行

6.2 線程、進程、協程對比

  1. 進程是資源分配的單位
  2. 線程是操做系統調度的單位
  3. 進程切換須要的資源很大,效率很低
  4. 線程切換須要的資源通常,效率通常(固然了在不考慮GIL的狀況下)
  5. 協程切換任務資源很小,效率高
  6. 多進程、多線程根據CPU核數不同多是並行的,可是協程是在一個線程中因此是併發

7. 阻塞和非阻塞

  1. Python中遇到recv、accept、等語句時,是默認阻塞的,即若是不設置一些條件時,程序會一直等待下去。

  2. 而非阻塞就是經過設置條件,把阻塞變爲非阻塞,因此轉變語法須要在阻塞以前設置

    例:server_socket.setblocking(False)
    複製代碼

8. Http協議

8.1 瀏覽器向服務器發送http請求

請求包括:請求頭、請求體、請求行

8.2 服務器向瀏覽器返回HTTP響應

響應包括:響應頭、響應行、響應體

9 長鏈接和短鏈接

9.1 短鏈接

短鏈接就是創建鏈接、接收數據、關閉鏈接,每次只獲取1次數據

9.2 長鏈接

長鏈接就是創建鏈接後,屢次請求,直到沒有請求後關閉的鏈接

代碼最後有close操做的其實都是短鏈接,長鏈接不能在鏈接中強制調用close

10. 三次握手,四次揮手

TCP通訊的整個過程

三次握手:創建鏈接時,客戶端向服務器發送鏈接請求,服務器向客戶端迴應請求的同時向客戶端發送鏈接請求,客戶端迴應請求,服務器收到時,三次握手成功,雙方鏈接成功。

四次揮手:客戶端調用close時,向服務器發送請求,服務器迴應請求同時解堵塞,調用本身close後,再次向客戶端發送close請求,此時客戶端方會等待兩個最大報文時間,等待接收服務器的請求(等待是爲了不斷網,斷電等特殊狀況),收到服務器的請求後,向服務器迴應請求,服務器收到請求後關閉,4次揮手成功,雙方關閉鏈接

相關文章
相關標籤/搜索