1. 程式人生 > 其它 >深度解析Python執行緒和程序

深度解析Python執行緒和程序

程序是系統進行資源分配和排程的一個獨立單位 程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程序是系統進行資源分配和排程的一個獨立單位。每個程序都有自己的獨立記憶體空間,不同程序通過程序間通訊來通訊。由於程序比較重量,佔據獨立的記憶體,所以上下文程序間的切換開銷(棧、暫存器、虛擬記憶體、檔案控制代碼等)比較大,但相對比較穩定安全。

什麼是程序

程序是系統進行資源分配和排程的一個獨立單位 程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程序是系統進行資源分配和排程的一個獨立單位。每個程序都有自己的獨立記憶體空間,不同程序通過程序間通訊來通訊。由於程序比較重量,佔據獨立的記憶體,所以上下文程序間的切換開銷(棧、暫存器、虛擬記憶體、檔案控制代碼等)比較大,但相對比較穩定安全。

什麼是執行緒

CPU排程和分派的基本單位 執行緒是程序的一個實體,是CPU排程和分派的基本單位,它是比程序更小的能獨立執行的基本單位.執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。執行緒間通訊主要通過共享記憶體,上下文切換很快,資源開銷較少,但相比程序不夠穩定容易丟失資料。

程序與執行緒的關係圖
執行緒與程序的區別:
  • 地址空間和其他資源:程序間相互獨立,同一程序的各執行緒間共享。某執行緒內的想愛你城咋其他程序不可見。
  • 通訊:程序間通訊IPC,執行緒間可以直接讀寫程序資料段來進行通訊——需要程序同步和互斥手段的輔助,以保證資料的一致性。
  • 排程和切換:執行緒上下文切換比程序上下文切換要快得多。
  • 在多執行緒作業系統中,程序不是一個可執行的實體

程序


程序的引入

現實生活中,有很多的場景中的事情是同時進行的,比如開車的時候 手和腳共同來駕駛汽車,比如唱歌跳舞也是同時進行的,再比如邊吃飯邊打電話;試想如果我們吃飯的時候有一個領導來電,我們肯定是立刻就接聽了。但是如果你吃完飯再接聽或者回電話,很可能會被開除。

# 模擬吃飯打電話

from time import sleep

def eating():
	 for i in range(1,6):
	 print("正在吃飯用時%d分鐘"% i)
	 sleep(1)

def call():
 	print('主人來電話了...')
  	for i in range(5):
  	print("接聽電話中...%d" % i)
    sleep(1)
if __name__ == '__main__':
    eating()  # 唱歌
    call() # 跳舞

注意:

很顯然剛剛的程式並沒有完成吃飯和接電話同時進行的要求
如果想要實現吃飯和接電話同時進行,那麼就需要一個新的方法,叫做:多工

多工的概念

什麼叫多工呢?簡單地說,就是作業系統可以同時執行多個任務。打個比方,你一邊在用瀏覽器上網,一邊在聽MP3,一邊在用Word趕作業,這就是多工,至少同時有3個任務正在執行。還有很多工悄悄地在後臺同時執行著,只是桌面上沒有顯示而已。

現在,多核CPU已經非常普及了,但是,即使過去的單核CPU,也可以執行多工。由於CPU執行程式碼都是順序執行的,那麼,單核CPU是怎麼執行多工的呢?
答案就是作業系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務2,任務2執行0.01秒,再切換到任務3,執行0.01秒,這樣反覆執行下去。表面上看,每個任務都是交替執行的,但是,由於CPU的執行速度實在是太快了,我們感覺就像所有任務都在同時執行一樣。
真正的並行執行多工只能在多核CPU上實現,但是,由於任務數量遠遠多於CPU的核心數量,所以,作業系統也會自動把很多工輪流排程到每個核心上執行。其實就是CPU執行速度太快啦!以至於我們感受不到在輪流排程。

並行與併發
並行(Parallelism)

並行:指兩個或兩個以上事件(或執行緒)在同一時刻發生,是真正意義上的不同事件或執行緒在同一時刻,在不同CPU資源呢上(多核),同時執行。

特點
  • 同一時刻發生,同時執行。
  • 不存在像併發那樣競爭,等待的概念。
併發(Concurrency)

指一個物理CPU(也可以多個物理CPU) 在若干道程式(或執行緒)之間多路複用,併發性是對有限物理資源強制行使多使用者共享以提高效率。

特點
  • 微觀角度:所有的併發處理都有排隊等候,喚醒,執行等這樣的步驟,在微觀上他們都是序列被處理的,如果是同一時刻到達的請求(或執行緒)也會根據優先順序的不同,而先後進入佇列排隊等候執行。
  • 巨集觀角度:多個幾乎同時到達的請求(或執行緒)在巨集觀上看就像是同時在被處理。
Python中程序操作
multiprocess.Process模組

process模組是一個建立程序的模組,藉助這個模組,就可以完成程序的建立。

  • 語法:Process([group [, target [, name [, args [, kwargs]]]]])

由該類例項化得到的物件,表示一個子程序中的任務(尚未啟動)。

注意:

  1. 必須使用關鍵字方式來指定引數;
  2. args指定的為傳給target函式的位置引數,是一個元祖形式,必須有逗號。

引數介紹:

  • group:引數未使用,預設值為None。
  • target:表示呼叫物件,即子程序要執行的任務。
  • args:表示呼叫的位置引數元祖。
  • kwargs:表示呼叫物件的字典。如kwargs = {'name':Jack, 'age':18}。
  • name:子程序名稱。

程式碼:

import os
from multiprocessing import Process


def func_one():
    print("第一個子程序")
    print("子程序(一)大兒子:%s  父程序:%s" % (os.getpid(), os.getppid()))


def func_two():
    print("第二個子程序")
    print("子程序(二)二兒子:%s  父程序:%s" % (os.getpid(), os.getppid()))


if __name__ == '__main__':
    p_one = Process(target=func_one)
    P_two = Process(target=func_two)
    p_one.start()
    P_two.start()
    print("子程序:%s  父程序:%s" % (os.getpid(), os.getppid()))  

除了上面這些開啟程序的方法之外,還有一種以繼承Process的方式開啟程序的方式:

import os
from multiprocessing import Process


class MyProcess(Process):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print("程序為%s,父程序為%s" % (os.getpid(), os.getppid()))
        print("我的名字是%s" % self.name)


if __name__ == '__main__':
    p_one = MyProcess('運動員A')
    p_two = MyProcess('運動員B')
    p_thr = MyProcess('運動員C')

    p_one.start()  # 自動呼叫run()
    p_two.start()
    p_thr.run()  # 直接呼叫run()

    p_one.join()
    p_two.join()
    # p_thr.join()  # 呼叫run()函式的不可以呼叫join()

    print("主程序結束")
鎖——Lock

通過上面的研究,我們千方百計實現了程式的非同步,讓多個任務可以同時在幾個程序中併發處理,他們之間的執行沒有順序,一旦開啟也不受我們控制。儘管併發程式設計讓我們能更加充分的利用IO資源,但是也給我們帶來了新的問題。

當多個程序使用同一份資料資源的時候,就會引發資料安全或順序混亂問題,我們可以考慮加鎖,我們以模擬搶票為例,來看看資料安全的重要性。

from multiprocessing import Process, Lock
import time
import json
import random


# 查詢票 
def search():
    dic = json.load(open('db'))  # 載入資料庫或資料檔案資料
    time.sleep(random.random())  # 模擬讀取資料
    print("剩餘票數:%s" % dic['count'])


# 買票
def get():
    dic = json.load(open('db'))  # 載入資料庫或資料檔案資料
    time.sleep(random.random())  # 模擬網路延遲
    if dic['count'] > 0:
        dic['count'] -= 1  # 購票成功後減一
        time.sleep(1)
        json.dump(dic, open('db', 'w'))
        print("購票成功")
    else:
        print("尚無餘票")


# 封裝成任務 ,在加鎖期間沒有,其他程序是無法操作資料的
def task(lock):
    lock.acquire()  # 請求加鎖
    search()
    get()
    lock.release()  # 釋放鎖


if __name__ == '__main__':
    lock = Lock()
    for i in range(10):
        p = Process(target=task, args=(lock,))
        p.start()

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

因此我們最好找尋一種解決方案能夠兼顧:1、效率高(多個程序共享一塊記憶體的資料)2、幫我們處理好鎖問題。

mutiprocessing模組為我們提供的基於訊息的IPC通訊機制:佇列和管道。佇列和管道都是將資料存放於記憶體中 佇列又是基於(管道+鎖)實現的,可以讓我們從複雜的鎖問題中解脫出來, 我們應該儘量避免使用共享資料,儘可能使用訊息傳遞和佇列,避免處理複雜的同步和鎖問題,而且在程序數目增多時,往往可以獲得更好的可獲展性(後續擴充套件該內容)。

執行緒


Python的threading模組

Python 供了幾個用於多執行緒程式設計的模組,包括 thread, threading 和 Queue 等。thread 和 threading 模組允許程式設計師建立和管理執行緒。thread 模組 供了基本的執行緒和鎖的支援,而 threading 供了更高級別,功能更強的執行緒管理的功能。Queue 模組允許使用者建立一個可以用於多個執行緒之間 共享資料的佇列資料結構。

python建立和執行執行緒
建立執行緒程式碼
  1. 建立方法一:
import os
import time
from threading import Thread,current_thread


def task1():
    for i in range(5):
        print('{}洗衣服:'.format(current_thread().name), i, os.getpid(), os.getppid())
        time.sleep(0.5)


def task2(n):
    for i in range(n):
        print('{}勞動最光榮,掃地中...'.format(current_thread().name), i, os.getpid(), os.getppid())
        time.sleep(0.5)


if __name__ == '__main__':
    print('main:', os.getpid())
    # 建立執行緒物件
    t1 = Thread(target=task1,name='警察')
    t2 = Thread(target=task2,name='小偷', args=(6,))
    # 啟動執行緒
    t1.start()
    t2.start()
  1. 建立方法二:
import time
from threading import Thread

# 自定義執行緒類
class MyThread(Thread):
    def __init__(self, name):
        Thread.__init__(self)
        self.name = name

    def run(self):
        for i in range(5):
            print('{}正在列印:{}'.format(self.name, i))
            time.sleep(0.1)


if __name__ == '__main__':

    # 建立三個執行緒,給執行緒起名字

    t1 = MyThread('小明')
    t2 = MyThread('小花')
    t3 = MyThread('ergou')

    # 啟動執行緒
    t1.start()
    t2.start()
    t3.start()

資源共享問題

程序和執行緒都是實現多工的一種方式,例如:在同一臺計算機上能同時執行多個QQ(程序),一個QQ可以開啟多個聊天視窗(執行緒)。資源共享:程序不能共享資源,而執行緒共享所在程序的地址空間和其他資源,同時,執行緒有自己的棧和棧指標。所以在一個程序內的所有執行緒共享全域性變數,但多執行緒對全域性變數的更改會導致變數值得混亂。

程式碼演示:

from threading import  Thread
import time
g_num=1000
def work1():
    global g_num
    g_num+=3
    print("work1----num:",g_num)

def work2():
    global g_num
    print("work2---num:",g_num)

if __name__ == '__main__':
    print("start---num:",g_num)
    t1=Thread(target=work1)
    t1.start()

    #故意停頓一秒,以保證執行緒1執行完成
    time.sleep(1)

    t2=Thread(target=work2)
    t2.start()

得到的結果是:

start---num: 1000

work1----num: 1003

work2---num: 1003
全域性直譯器鎖(GIL)

首先需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就好比C++是一套語言(語法)標準,但是可以用不同的編譯器來編譯成可執行程式碼。同樣一段程式碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行(其中的JPython就沒有GIL)。

那麼CPython實現中的GIL又是什麼呢?GIL全稱Global Interpreter Lock為了避免誤導,我們還是來看一下官方給出的解釋:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
  • 主要意思為:

GIL是一個互斥鎖,它防止多個執行緒同時執行Python位元組碼。這個鎖是必要的,主要是因為CPython的記憶體管理不是執行緒安全的 儘管Python完全支援多執行緒程式設計, 但是直譯器的C語言實現部分在完全並行執行時並不是執行緒安全的。

因此,直譯器實際上被一個全域性直譯器鎖保護著,它確保任何時候都只有一個Python執行緒執行。在多執行緒環境中,Python 虛擬機器按以下方式執行:

  • 設定GIL
  • 切換到一個執行緒去執行
  • 執行

由於GIL的存在,Python的多執行緒不能稱之為嚴格的多執行緒。因為多執行緒下每個執行緒在執行的過程中都需要先獲取GIL,保證同一時刻只有一個執行緒在執行。

由於GIL的存在,即使是多執行緒,事實上同一時刻只能保證一個執行緒在執行,既然這樣多執行緒的執行效率不就和單執行緒一樣了嗎,那為什麼還要使用多執行緒呢?

由於以前的電腦基本都是單核CPU,多執行緒和單執行緒幾乎看不出差別,可是由於計算機的迅速發展,現在的電腦幾乎都是多核CPU了,最少也是兩個核心數的,這時差別就出來了:通過之前的案例我們已經知道,即使在多核CPU中,多執行緒同一時刻也只有一個執行緒在執行,這樣不僅不能利用多核CPU的優勢,反而由於每個執行緒在多個CPU上是交替執行的,導致在不同CPU上切換時造成資源的浪費,反而會更慢。即原因是一個程序只存在一把gil鎖,當在執行多個執行緒時,內部會爭搶gil鎖,這會造成當某一個執行緒沒有搶到鎖的時候會讓cpu等待,進而不能合理利用多核cpu資源。

但是在使用多執行緒抓取網頁內容時,遇到IO阻塞時,正在執行的執行緒會暫時釋放GIL鎖,這時其它執行緒會利用這個空隙時間,執行自己的程式碼,因此多執行緒抓取比單執行緒抓取效能要好,所以我們還是要使用多執行緒的。

GIL對多執行緒Python程式的影響

程式的效能受到計算密集型(CPU)的程式限制和I/O密集型的程式限制影響,那什麼是計算密集型和I/O密集型程式呢?

計算密集型:要進行大量的數值計算,例如進行上億的數字計算、計算圓周率、對視訊進行高清解碼等等。這種計算密集型任務雖然也可以用多工完成,但是花費的主要時間在任務切換的時間,此時CPU執行任務的效率比較低。

IO密集型:涉及到網路請求(time.sleep())、磁碟IO的任務都是IO密集型任務,這類任務的特點是CPU消耗很少,任務的大部分時間都在等待IO操作完成(因為IO的速度遠遠低於CPU和記憶體的速度)。對於IO密集型任務,任務越多,CPU效率越高,但也有一個限度。

當然為了避免GIL對我們程式產生影響,我們也可以使用,執行緒鎖。

Lock&RLock

常用的資源共享鎖機制:有Lock、RLock、Semphore、Condition等,簡單給大家分享下Lock和RLock。

Lock
  • Lock 不能連續acquire鎖,不然會死鎖,Lock 資源競爭可能會導致死鎖。
  • Lock 會降低效能。
from threading import Thread, Lock
lock = Lock()
total = 0
# 兩個執行緒共用一把鎖,其中通過acquire申請獲取鎖物件,通過release釋放鎖資源

# 進行加法
def add():
    global total
    global lock
    for i in range(1000000):
        lock.acquire()
        total += 1
        lock.release()
# 進行減法    
def sub():
    global total
    global lock
    for i in range(1000000):
        lock.acquire()
        total -= 1
        lock.release()

# 建立執行緒物件    
thread1 = Thread(target=add)
thread2 = Thread(target=sub)

# 將Thread1和2設定為守護執行緒,主執行緒完成時,子執行緒也一起結束
# thread1.setDaemon(True)
# thread1.setDaemon(True)

# 啟動執行緒
thread1.start()
thread2.start()

# 阻塞,等待執行緒1和2完成,如果不使用join,那麼主執行緒完成後,子執行緒也會自動關閉。
thread1.join()
thread2.join()
特點就是執行速度慢,但是保證了資料的安全性

RLock

  • RLock 可以連續acquire鎖,但是需要相應數量的release釋放鎖
  • 因可以連續獲取鎖,所以實現了函式內部呼叫帶鎖的函式
from threading import Thread, Lock, RLocklock = RLock()total = 0def add():    global lock    global total    
# RLock實現連續獲取鎖,但是需要相應數量的release來釋放資源    
for i in range(1000000):    
# 可以連續獲取鎖        
lock.acquire()        
lock.acquire()        
total += 1       
# 要有物件的release        
lock.release()        
lock.release()def sub():    
global lock    
global total    
for i in range(1000000):        
lock.acquire()        
total -= 1        
lock.release()thread1 = Thread(target=add)thread2 = Thread(target=sub)thread1.start()thread2.start()# 阻塞,等待執行緒1和2完成,如果不使用join,那麼主執行緒完成後,子執行緒也會自動關閉。thread1.join()thread2.join()

使用鎖程式碼操作不當就會產生死鎖的情況。

什麼是死鎖

死鎖:當執行緒A持有獨佔鎖a,並嘗試去獲取獨佔鎖b的同時,執行緒B持有獨佔鎖b,並嘗試獲取獨佔鎖a的情況下,就會發生AB兩個執行緒由於互相持有對方需要的鎖,而發生的阻塞現象,我們稱為死鎖。即死鎖是指多個程序因競爭資源而造成的一種僵局,若無外力作用,這些程序都將無法向前推進。

死鎖的原因
  • 競爭系統資源
  • 程序執行推進的順序不當
  • 資源分配不當
產生死鎖的四個必要條件
  • 互斥條件:一個資源每次只能被一個程序使用
  • 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
  • 不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。
  • 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
解決死鎖的辦法
  • 減少資源佔用時間,可以降低死鎖放生的概率。
  • 銀行家演算法。銀行家演算法的本質是優先滿足佔用資源較少的任務。
  • 理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地避免、預防和解除死鎖。

所以,在系統設計、程序排程等方面注意如何不讓這四個必要條件成立,如何確定資源的合理分配演算法,避免程序永久佔據系統資源。

死鎖程式碼

def task1(lock1, lock2):
    if lock1.acquire():
        print('{}獲取到lock1鎖。。。。'.format(current_thread().name))
        for i in range(5):
            print('{}------------->{}'.format(current_thread().name, i))
            time.sleep(0.01)
        if lock2.acquire(timeout=2):
            print('{}獲取了lock1,lock2'.format(current_thread().name))
            lock2.release()

        lock1.release()


def task2(lock1, lock2):
    if lock2.acquire():
        print('{}獲取到lock2鎖。。。。'.format(current_thread().name))
        for i in range(5):
            print('{}----->{}'.format(current_thread().name, i))
            time.sleep(0.01)
        if lock1.acquire(timeout=2):
            print('{}獲取了lock1,lock2'.format(current_thread().name))
            lock1.release()
        lock2.release()


if __name__ == '__main__':
    lock1 = Lock()
    lock2 = Lock()

    t1 = Thread(target=task1, args=(lock1, lock2))
    t2 = Thread(target=task2, args=(lock1, lock2))

    t1.start()
    t2.start()
python執行緒間通訊

如果各個執行緒之間各幹各的,確實不需要通訊,這樣的程式碼也十分的簡單。但這一般是不可能的,至少執行緒要和主執行緒進行通訊,不然計算結果等內容無法取回。而實際情況中要複雜的多,多個執行緒間需要交換資料,才能得到正確的執行結果。

Queue訊息佇列

python中Queue是訊息佇列,提供執行緒間通訊機制,python3中重名為為queue,queue模組塊下提供了幾個阻塞佇列,這些佇列主要用於實現執行緒通訊。

在 queue 模組下主要提供了三個類,分別代表三種佇列,它們的主要區別就在於進佇列、出佇列的不同。

  • Queue(maxsize=0):建立一個FIFO佇列,若給定最大值,佇列沒有空間時阻塞,否則是無限佇列
  • LifoQueue(maxsize=0):建立一個棧,maxsize含義同上
  • PriorityQueue(maxsize=0):建立一個優先佇列,maxsize含義同上
Queue物件方法:
  • qsize():返回佇列大小,是近似值(返回時可能佇列大小被修改了)
  • empty():判斷佇列是否為空
  • full():判斷佇列是否為滿
  • put(item, block=True, timeout=None):將item加入佇列,可選阻塞和阻塞時間
  • put_nowait(item):即put(item, False)
  • get(block=True, timeout=None):從佇列中獲取元素,可選阻塞和阻塞時間
  • get_nowait():即get(False)
  • task_done():用於表示佇列中某個元素已經執行完成,會被join()呼叫
  • join():佇列中所有元素執行完畢並呼叫task_done()訊號之前,保持阻塞
Queue模組異常:
  • Empty:對空佇列呼叫 get(timeout=n),如果等待n秒鐘佇列還是空的就會丟擲異常

  • Full:對滿佇列呼叫put(item,timeout=n),如果等待n秒鐘仍然是滿的就會丟擲異常

  • 簡單程式碼演示

import random
import time
from queue import Queue

queue = Queue(3)

queue.put('香蕉')
queue.put('榴蓮')
queue.put('西瓜')
queue.put('蘋果')

print(queue.get())
print(queue.get())
print(queue.get())

此時程式碼會阻塞,因為queue中內容已滿,此時可以在第四個queue.put('蘋果')後面新增timeout,則成為 queue.put('蘋果',timeout=1)如果等待1秒鐘仍然是滿的就會丟擲異常,可以捕獲異常。

import random
import time
from queue import Queue

queue = Queue(3)
try:
    queue.put('香蕉')
    queue.put('榴蓮')
    queue.put('西瓜')
    queue.put('蘋果',timeout=1)

    print(queue.get())
    print(queue.get())
    print(queue.get())

except Exception as e:
    print(e)

同理如果佇列是空的,無法獲取到內容預設也會阻塞,如果不阻塞可以使用queue.get_nowait()。

使用Queue完成執行緒間通訊

在掌握了 Queue 阻塞佇列的特性之後,在下面程式中就可以利用 Queue 來實現執行緒通訊了。

下面演示一個生產者和一個消費者,當然都可以多個

import random
import time
from threading import Thread, current_thread
from queue import Queue


def producer(queue):
    print('{}開門啦!'.format(current_thread().name))
    foods = ['紅燒獅子頭', '香腸烤飯', '蒜蓉生蠔', '酸辣土豆絲', '肉餅']
    for i in range(1, 21):
        food = random.choice(foods)
        print('{}正在加工中.....'.format(food))
        time.sleep(1)
        print('加工完成可以上菜了...')
        queue.put(food)
    queue.put(None)


def consumer(queue):
    print('{}來吃飯啦'.format(current_thread().name))
    while True:
        food = queue.get()
        if food:
            print('正在享用美食:', food)
            time.sleep(0.5)
        else:
            print('{}把飯店吃光啦,走人...'.format(current_thread().name))
            break


if __name__ == '__main__':
    queue = Queue(8)
    t1 = Thread(target=producer, name='老家肉餅', args=(queue,))
    t2 = Thread(target=consumer, name='坤坤', args=(queue,))

    t1.start()
    t2.start()

使用queue模組,可線上程間進行通訊,並保證了執行緒安全。

協程


協程,又稱微執行緒,纖程。英文名Coroutine。

什麼是協程

協程是python箇中另外一種實現多工的方式,只不過比執行緒更小佔用更小執行單元(理解為需要的資源)。為啥說它是一個執行單元,因為它自帶CPU上下文。這樣只要在合適的時機, 我們可以把一個協程 切換到另一個協程。只要這個過程中儲存或恢復 CPU上下文那麼程式還是可以執行的。
通俗的理解:在一個執行緒中的某個函式,可以在任何地方儲存當前函式的一些臨時變數等資訊,然後切換到另外一個函式中執行,注意不是通過呼叫函式的方式做到的,並且切換的次數以及什麼時候再切換到原來的函式都由開發者自己確定。

協程和執行緒差異

在實現多工時,執行緒切換從系統層面遠不止儲存和恢復 CPU上下文這麼簡單。作業系統為了程式執行的高效性每個執行緒都有自己快取Cache等等資料,作業系統還會幫你做這些資料的恢復操作。所以執行緒的切換非常耗效能。但是協程的切換隻是單純的操作CPU的上下文,所以一秒鐘切換個上百萬次系統都抗的住。

# 簡單實現協程
import time

def task1():
    while True:
        print("----task1---")
        yield
        time.sleep(0.5)

def task2():
    while True:
        print("----task2---")
        yield
        time.sleep(0.5)

def main():
    w1 = task1()
    w2 = task2()
    while True:
        next(w1)
        next(w2)

if __name__ == "__main__":
    main()
greenlet與gevent

為了更好使用協程來完成多工,除了使用原生的yield完成模擬協程的工作,其實python還有的greenlet模組和gevent模組,使實現協程變的更加簡單高效。

greenlet雖說實現了協程,但需要我們手工切換,太麻煩了,gevent是比greenlet更強大的並且能夠自動切換任務的模組。

其原理是當一個greenlet遇到IO(指的是input output 輸入輸出,比如網路、檔案操作等)操作時,比如訪問網路,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。

安裝:
pip3 install gevent

模擬耗時操作:

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #用來模擬一個耗時操作,注意不是time模組中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

如果有耗時操作也可以換成,gevent中自己實現的模組,這時候就需要打補丁了。

from gevent import monkey
# 有耗時操作時需要
monkey.patch_all()  # 而且要放在程式碼的前面
....

使用協程完成一個簡單的二手房資訊的爬蟲程式碼吧!

import urllib.request
import ssl
import random
import time
import os
import gevent
from gevent import monkey

monkey.patch_all()


def gettext(url):
    r
    agentlist = [
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:67.0) Gecko/20100101 Firefox/67.0"]
    request = urllib.request.Request(url, headers={"User-Agent": random.choice(agentlist)})
    response = urllib.request.urlopen(request, context=context)
    return response.read().decode()


def func(url):  # url = "https://bj.lianjia.com/ershoufang/pg2"
    with open(url.rsplit("/", maxsplit=1)[-1] + ".html", "w", encoding="utf-8") as f:
        f.write(gettext(url))


if __name__ == '__main__':
    '''
    爬取前30頁,並將爬取的內容儲存到指定的資料夾
    '''
    start = time.time()
    os.mkdir("ershoufang1")
    os.chdir("ershoufang1")
    url = "https://bj.lianjia.com/ershoufang"
    urlhead = "https://bj.lianjia.com/ershoufang/pg"
    glist = []
    for i in range(1, 11):
        url = urlhead + str(i)
        g = gevent.spawn(func, url)
        glist.append(g)

    for g in glist:
        g.join()
    end = time.time()
    print(end - start)
我唯一的害怕,是你們已經不相信了——不相信規則能戰勝潛規則,不相信學場有別於官場,不相信學術不等於權術,不相信風骨遠勝於媚骨,在這個懷疑的時代,我們仍然要有信仰,信仰努力而不是運氣,這個世界雖然不夠純潔,但我仍然相信它還不能埋沒真正有才華的人