這篇文章揭開python進程、線程、協程神祕的面紗

1、概念

關注公衆號“輕鬆學編程”瞭解更多。
回覆“協程”獲取本文源代碼。】

從計算機硬件角度:

計算機的核心是CPU,承擔了所有的計算任務。
一個CPU,在一個時間切片裏只能運行一個程序。
在這裏插入圖片描述

															圖1. 操作系統

在這裏插入圖片描述

1.1 進程

進程:是CPU對程序的一次執行過程、一次執行任務。各個進程有自己的內存空間、數據棧等。操作系統分配內存的基本單位(打開、執行、保存…)

1.2 線程

線程:是進程中執行運算的最小單位,是進程中的一個實體。(打開、執行、保存…)
一個程序至少有一個進程,一個進程至少有一個線程。
操作系統分配CPU的基本單位

1.3 協程

協程:比線程更小的執行單元,又稱微線程,在單線程上執行多個任務,自帶CPU上下文

用函數切換,開銷極小。不通過操作系統調度,
沒有進程、線程的切換開銷。(gevent,monkey.patchall)

舉例

我們假設把一個進程比作我們實際生活中的一個拉麪館,負責保持拉麪館運行的服務員就是線程,每個餐桌代表要完成的任務。

當我們用多線程完成任務時,模式是這樣的:每來一桌的客人,就在那張桌子上安排一個服務員,即有多少桌客人就得對應多少個服務員;

而當我們用協程來完成任務時,模式卻有所不同了: 就安排一個服務員,來喫飯得有一個點餐和等菜的過程,當A在點菜,就去B服務,B叫了菜在等待,我就去C,當C也在等菜並且A點菜點完了,趕緊到A來服務… …依次類推。

從上面的例子可以看出,想要使用協程,那麼我們的任務必須有等待。當我們要完成的任務有耗時任務,屬於IO密集型任務時,我們使用協程來執行任務會節省很多的資源(一個服務員和多個服務員的區別,並且可以極大的利用到系統的資源。

1.4 線程安全

多線程環境中,共享數據同一時間只能有一個線程來操作。

1.5 原子操作

原子操作就是不會因爲進程併發或者線程併發而導致被中斷的操作。

1.6 並行和併發

串行:單個CPU核心,按順序執行

並行:多個CPU核心,不同的程序就分配給不同的CPU來運行。可以讓多個程序同時執行。(多進程)

併發:單個CPU核心,在一個時間切片裏一次只能運行一個程序,如果需要運行多個程序,則串行執行,遇到IO阻塞就切換,即計算機在邏輯上能處理多任務的能力。(多進程,多線程)
在這裏插入圖片描述

1.7 多進程/多線程

表示可以同時執行多個任務,進程和線程的調度是由操作系統自動完成。

進程:每個進程都有自己獨立的內存空間,不同進程之間的內存空間不共享。

線程:一個進程可以有多個線程,所有線程共享進程的內存空間,通訊效率高,切換開銷小。共享意味着競爭,導致數據不安全,爲了保護內存空間的數據安全,引入"互斥鎖",“遞歸鎖”,“升序鎖”等。

在這裏插入圖片描述
在這裏插入圖片描述

1.8 Python的多線程:

GIL:Global Interpreter Lock, 全局解釋器鎖,線程的執行權限,在Python的進程裏只有一個GIL。

在這裏插入圖片描述
在這裏插入圖片描述
一個線程需要執行任務,必須獲取GIL。

好處:直接杜絕了多個線程訪問內存空間的安全問題。

壞處:Python的多線程不是真正多線程,不能充分利用多核CPU的資源。

但是,在I/O阻塞的時候,解釋器會釋放GIL。

1.9 同步、異步、阻塞、非阻塞

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

異步,異步本質上是單線程的,因爲 IO 操作在很多時候會存在阻塞,異步就是在這種阻塞的時候,通過控制權的交換來實現多任務的。即異步本質上是運行過程中的控制權的交換。最典型的例子就是生產者消費者模型。

同步,即程序協同進行,遇到阻塞就等待,直到任務完成爲止。

2.0 運用場景

多進程:密集CPU任務,需要充分使用多核CPU資源(服務器,大量的並行計算)的時候,用多進程。 multiprocessing

缺陷:多個進程之間通信成本高,切換開銷大。

多線程:密集I/O任務(網絡I/O,磁盤I/O,數據庫I/O)使用多線程合適。

threading.Thread、multiprocessing.dummy

缺陷:同一個時間切片只能運行一個線程,不能做到高並行,但是可以做到高併發。

在這裏插入圖片描述

協程:又稱微線程(一種用戶態的輕量級線程),在單線程上執行多個任務,用函數切換,由程序自身控制,開銷極小。

不通過操作系統調度,沒有進程、線程的切換開銷。

每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置,不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。

當程序中存在大量不需要CPU的操作時(IO),遇到IO操作自動切換到其它協程。

greenletgeventmonkey.patchallyieldasync

多線程請求返回是無序的,哪個線程有數據返回就處理哪個線程,而協程返回的數據是有序的。

因爲協程是一個線程執行,所以想要利用多核CPU,最簡單的方法是多進程+協程,這樣既充分利用多核,又充分發揮協程的高效率。

缺陷:單線程執行,處理密集CPU和本地磁盤IO的時候,性能較低。處理網絡I/O性能還是比較高.

2.1 互斥鎖、遞歸鎖、升序鎖

Python的GIL只能保證原子操作的線程安全,因此在多線程編程時我們需要通過加鎖來保證線程安全。

最簡單的鎖是互斥鎖(同步鎖),互斥鎖是用來解決IO密集型場景產生的計算錯誤,即目的是爲了保護共享的數據,同一時間只能有一個線程來修改共享的數據。

遞歸鎖:就是在一個大鎖中再包含子鎖

升序鎖:兩個線程想獲取到的鎖,都被對方線程拿到了,那麼我們只需要保證在這兩個線程中,獲取鎖的順序保持一致就可以了。舉個例子,我們有線程thread_a, thread_b, 鎖lock_1, lock_2。只要我們規定好了鎖的使用順序,比如先用lock_1,再用lock_2,當線程thread_a獲得lock_1時,其他線程如thread_b就無法獲得lock_1這個鎖,也就無法進行下一步操作(獲得lock_2這個鎖),也就不會導致互相等待導致的死鎖。簡言之,解決死鎖問題的一種方案是爲程序中的每一個鎖分配一個唯一的id,然後只允許按照升序規則來使用多個鎖,這個規則使用上下文管理器 是非常容易實現的。

2.2 代碼

創建進程

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 創建進程.py
    @time: 2020/3/3 12:22
    @author:LDC
    '''
    
    import multiprocessing
    import time
    
    
    def func(arg):
        pname = multiprocessing.current_process().name  # 獲取當前進程名稱
        pid = multiprocessing.current_process().pid  # 獲取當前進程id
        print("當前進程ID=%d,name=%s" % (pid, pname))
    
        for i in range(5):
            print(pname, pid, arg)
            time.sleep(1)
    
        pass
    
    
    if __name__ == "__main__":
        pname = multiprocessing.current_process().name
        pid = multiprocessing.current_process().pid
        print("當前進程ID=%d,name=%s" % (pid, pname))
    
        p = multiprocessing.Process(target=func, name='我是子進程', args=("hello",))
        p.daemon = True  # 設爲【守護進程】(隨主進程的結束而結束)
        p.start()
    
        while True:
            print("子進程是否活着?", p.is_alive())
            if not p.is_alive():
                break
            time.sleep(1)
            pass
    
        print("main over")

進程間的通訊

每個進程都擁有自己的內存空間,因此不同進程間內存是不共享的,要想實現兩個進程間的數據交換,有幾種常用的方法:

Queue(隊列)

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 進程間的通訊.py
    @time: 2020/3/3 15:13
    @author:LDC
    '''
    
    from multiprocessing import Process, Queue, current_process
    
    def start(q):
        pname = current_process().name
        pid = current_process().pid
        print('當前進程是{}_{}'.format(pid, pname))
        # 從隊列中取出數據,先判斷隊列 是否爲空
        if not q.empty():
            print(q.get())
        # 存數據進隊列
        q.put('hello from {}_{}'.format(pid, pname))
    
    
    if __name__ == '__main__':
        q = Queue()
        p_list = []
        for i in range(0, 2):
            p = Process(target=start, args=(q,))
            p.start()
            p_list.append(p)
        # 確保所有進程執行完
        for p in p_list:
            p.join()

Manager(實現了進程間真正的數據共享):

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 進程間的通訊(manager).py
    @time: 2020/3/3 15:54
    @author:LDC
    '''
    
    from multiprocessing import Process, Manager, current_process
    
    
    def start(m_dict, m_list):
        pname = current_process().name
        pid = current_process().pid
        print('當前進程是{}_{}'.format(pid, pname))
        print(m_dict)
        m_dict[pid] = pname
        m_list.append(pid)
    
    
    
    if __name__ == '__main__':
        manager = Manager()
        m_dict = manager.dict()  # 通過manager生成一個字典
        m_list = manager.list()  # 通過manager生成一個列表
        p_list = []
        for i in range(10):
            p = Process(target=start, args=(m_dict, m_list))
            p.start()
            p_list.append(p)
        for res in p_list:
            res.join()
    
        print(m_dict)
        print(m_list)

進程池(多進程)

進程池內部維護一個進程序列,當使用時,則去進程池中獲取一個進程,如果進程池序列中沒有可供使用的進程,那麼程序就會等待,直到進程池中有可用進程爲止。

進程池中有兩個方法:

1、apply(同步)

2、apply_async(異步)

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 進程池.py
    @time: 2020/3/3 16:05
    @author:LDC
    '''
    
    from multiprocessing import Pool, current_process
    import time
    
    
    def Foo(i):
        pid = current_process().pid
        pname = current_process().name
        time.sleep(1)
        print('hello','{},{}_{}'.format(i, pid, pname))
        return '{},{}_{}'.format(i, pid, pname)
    
    
    def Bar(arg):
        print('number::', arg)
    
    
    if __name__ == "__main__":
        pool = Pool(3)  # 定義一個進程池,裏面有3個進程
        for i in range(10):
            # 使用異步線程時,需要定義一個回調函數,當執行完後把結果傳給回調函數
            pool.apply_async(func=Foo, args=(i,), callback=Bar)
            # pool.apply(func=Foo, args=(i,))
    
        pool.close()  # 關閉進程池
        pool.join()  # 進程池中進程執行完畢後再關閉,(必須先close在join)    

輸出:

    hello 0,12776_SpawnPoolWorker-1
    number:: 0,12776_SpawnPoolWorker-1
    hello 1,8832_SpawnPoolWorker-2
    hello 2,4704_SpawnPoolWorker-3
    number:: 1,8832_SpawnPoolWorker-2
    number:: 2,4704_SpawnPoolWorker-3
    hello 5,8832_SpawnPoolWorker-2
    hello 4,4704_SpawnPoolWorker-3
    number:: 5,8832_SpawnPoolWorker-2
    number:: 4,4704_SpawnPoolWorker-3
    hello 3,12776_SpawnPoolWorker-1
    number:: 3,12776_SpawnPoolWorker-1
    hello 6,8832_SpawnPoolWorker-2
    hello 7,4704_SpawnPoolWorker-3
    number:: 6,8832_SpawnPoolWorker-2
    number:: 7,4704_SpawnPoolWorker-3
    hello 8,12776_SpawnPoolWorker-1
    number:: 8,12776_SpawnPoolWorker-1
    hello 9,4704_SpawnPoolWorker-3
    number:: 9,4704_SpawnPoolWorker-3

callback是回調函數,就是在執行完Foo方法後會自動執行Bar函數,並且自動把Foo函數的返回值作爲參數傳入Bar函數.

多進程

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 多進程.py
    @time: 2020/3/3 16:19
    @author:LDC
    '''
    import time
    from concurrent import futures
    from multiprocessing import current_process
    
    
    def foo(i):
        pid = current_process().pid
        pname = current_process().name
        time.sleep(3)
        print('hello', '{},{}_{}'.format(i, pid, pname))
        return '{},{}_{}'.format(i, pid, pname)
    
    
    if __name__ == '__main__':
        i_list = [1, 2, 3, 4, 5]
        with futures.ProcessPoolExecutor(5) as executor:
    
            res = executor.map(foo, i_list)
            # to_do = [executor.submit(foo, item) for item in i_list]
            # ret = [future.result() for future in futures.as_completed(to_do)]

注意map可以保證輸出的順序, submit輸出的順序是亂的

如果你要提交的任務的函數是一樣的,就可以簡化成map。但是假如提交的任務函數是不一樣的,或者執行的過程之可能出現異常(使用map執行過程中發現問題會直接拋出錯誤)就要用到submit()

submit和map的參數是不同的,submit每次都需要提交一個目標函數和對應的參數,map只需要提交一次目標函數,目標函數的參數放在一個迭代器(列表,字典)裏就可以。

線程

    方法:
      start        線程準備就緒,等待CPU調度
      setName      設置線程名稱
      getName      獲取線程名稱
      setDaemon    把一個主進程設置爲Daemon線程後,主線程執行過程中,後臺線程也在進行,主線程執行完畢後,後臺線程不論有沒執行完成,都會停止
      join         逐個執行每個線程,執行完畢後繼續往下執行,該方法使得多線程變得無意義  
      run              線程被cpu調度後自動執行線程對象的run方法
    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 創建線程(常用).py
    @time: 2020/3/3 16:49
    @author:LDC
    '''
    
    import threading
    import time
    
    '''直接調用'''
    
    
    def foo(name):
        time.sleep(name)
        # 獲取當前線程名稱與標誌號
        print(threading.currentThread().name, threading.currentThread().ident)
        print("Hello %s" % name)
    
    
    
    if __name__ == "__main__":
        t_list = []
        # 創建了兩個線程
        for i in range(2):
            t = threading.Thread(target=foo, args=(i+2,), name='t_{}'.format(i))  # 生成線程實例
            # t.setDaemon(True)  # True表示子線程設置爲守護線程,主線程死去,子線程也跟着死去,不管是否執行完
            # t.setDaemon(False)  # False表示子線程設置爲非守護線程,主線程死去,子線程依然在執行
            t_list.append(t)
            t.start()
    
        # for t in t_list:
        #     t.join()  # 等待子線程執行完,
        #     print(t.getName())  # 獲取線程名

多線程

   # 可以使用for循環創建多個線程
    for i in range3):
        t = threading.Thread(target=foo, args=(i+2,), name='t_{}'.format(i))
        t.start()

線程池
爲什麼要使用線程池?

對於任務數量不斷增加的程序,每有一個任務就生成一個線程,最終會導致線程數量的失控,例如,整站爬蟲,假設初始只有一個鏈接a,那麼,這個時候只啓動一個線程,運行之後,得到這個鏈接對應頁面上的b,c,d,,,等等新的鏈接,作爲新任務,這個時候,就要爲這些新的鏈接生成新的線程,線程數量暴漲。在之後的運行中,線程數量還會不停的增加,完全無法控制。所以,對於任務數量不端增加的程序,固定線程數量的線程池是必要的。

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 線程池.py
    @time: 2020/3/3 20:36
    @author:LDC
    '''
    import time
    from concurrent.futures import ThreadPoolExecutor
    
    
    # 任務
    def doSth(args):
        
        print("hello", args)
        # time.sleep(2)
    
    
    if __name__ == '__main__':
        # max_workers 線程數
        argsList = (1, 2, 3, 4, 5, 6)
        # 使用sumbit()函數提交任務
        with ThreadPoolExecutor(5) as exe:
            for a in argsList:
                exe.submit(doSth, a)
        # 使用map()函數提交任務
        print("使用map()提交任務")
        with ThreadPoolExecutor(5) as exe:
            exe.map(doSth, argsList)

線程衝突

當多個線程同時訪問同一個變量時就可能造成線程衝突

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 線程衝突.py
    @time: 2020/3/3 20:57
    @author:LDC
    '''
    
    import threading
    
    money = 0
    
    
    def add_money():
        global money
        for i in range(10000000):
            money += 1
    
    
    if __name__ == '__main__':
        add_money()
        add_money()
        print('調用兩次函數money實際值爲:', money)
        money = 0
        t_list = []
        for i in range(2):
            t = threading.Thread(target=add_money)
            t.start()
            t_list.append(t)
        for t in t_list:
            t.join()
        print('使用線程後money實際值爲:', money)

輸出:

    調用兩次函數money實際值爲: 20000000
    使用線程後money實際值爲: 11656269

原因:當對全局資源存在寫操作時,如果不能保證寫入過程的原子性,會出現髒讀髒寫的情況,即線程不安全。Python的GIL只能保證原子操作的線程安全,因此在多線程編程時我們需要通過加鎖來保證線程安全。

最簡單的鎖是互斥鎖(同步鎖),互斥鎖是用來解決IO密集型場景產生的計算錯誤,即目的是爲了保護共享的數據,同一時間只能有一個線程來修改共享的數據。

互斥鎖

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 互斥鎖.py
    @time: 2020/3/3 21:08
    @author:LDC
    '''
    
    import threading
    
    lock = threading.Lock()
    money = 0
    
    def add_money():
        global money
        with lock:
            for i in range(10000000):
                money += 1
    
    if __name__ == '__main__':
        add_money()
        add_money()
        print('調用兩次函數money實際值爲:', money)
        money = 0
        t_list = []
        for i in range(2):
            t = threading.Thread(target=add_money)
            t.start()
            t_list.append(t)
        for t in t_list:
            t.join()
        print('使用線程後money實際值爲:', money)

鎖適用於訪問和修改同一個資源的時候,引起資源爭用的情況下。

使用鎖的注意事項:

1. 少用鎖,除非有必要。多線程訪問加鎖的資源時,由於鎖的存在,實際就變成了串行。
2. 加鎖時間越短越好,不需要就立即釋放鎖。
3. 一定要避免死鎖,使用with或者try...finally。

第一種死鎖:迭代死鎖

該情況是一個線程“迭代”請求同一個資源,直接就會造成死鎖。這種死鎖產生的原因是我們標準互斥鎖threading.Lock的缺點導致的。

標準的鎖對象(threading.Lock)並不關心當前是哪個線程佔有了該鎖;如果該鎖已經被佔有了,那麼任何其它嘗試獲取該鎖的線程都會被阻塞,包括已經佔有該鎖的線程也會被阻塞。

比如:

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 迭代死鎖.py
    @time: 2020/3/3 22:10
    @author:LDC
    '''
    
    import threading
    import time
    
    count_list = [0, 0]
    lock = threading.Lock()
    
    
    def change_0():
        global count_list
        with lock:
            tmp = count_list[0]
            time.sleep(0.001)
            count_list[0] = tmp + 1
            time.sleep(2)
            print("Done. count_list[0]:%s" % count_list[0])
    
    
    def change_1():
        global count_list
        with lock:
            tmp = count_list[1]
            time.sleep(0.001)
            count_list[1] = tmp + 1
            time.sleep(2)
            print("Done. count_list[1]:%s" % count_list[1])
    
    
    def change():
        with lock:
            change_0()
            time.sleep(0.001)
            change_1()
    
    
    def verify(sub):
        global count_list
        thread_list = []
        for i in range(5):
            t = threading.Thread(target=sub, args=())
            t.start()
            thread_list.append(t)
        for j in thread_list:
            j.join()
        print(count_list)
    
    
    if __name__ == "__main__":
        verify(change)

示例中,我們有一個共享資源count_list,有兩個分別取這個共享資源第一部分和第二部分的數字(count_list[0]和count_list[1])。兩個訪問函數都使用了鎖來確保在獲取數據時沒有其它線程修改對應的共享數據。
現在,如果我們思考如何添加第三個函數來獲取兩個部分的數據。一個簡單的方法是依次調用這兩個函數,然後返回結合的結果。

這裏的問題是,如有某個線程在兩個函數調用之間修改了共享資源,那麼我們最終會得到不一致的數據。

最明顯的解決方法是在這個函數中也使用lock。然而,這是不可行的。裏面的兩個訪問函數將會阻塞,因爲外層語句已經佔有了該鎖。

結果是沒有任何輸出,死鎖。

爲了解決這個問題,我們可以用遞歸鎖代替互斥鎖。

遞歸鎖

就是在一個大鎖中再包含子鎖。它相當於一個字典,記錄了鎖的門與鎖的對應值,當開門的時候會根據對應鑰匙來開鎖。

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 迭代死鎖.py
    @time: 2020/3/3 22:10
    @author:LDC
    '''
    
    import threading
    import time
    
    count_list = [0, 0]
    lock = threading.RLock() # 遞歸鎖
    
    
    def change_0():
        global count_list
        with lock:   # 小鎖
            tmp = count_list[0]
            time.sleep(0.001)
            count_list[0] = tmp + 1
            time.sleep(2)
            print("Done. count_list[0]:%s" % count_list[0])
    
    
    def change_1():
        global count_list
        with lock:   # 小鎖
            tmp = count_list[1]
            time.sleep(0.001)
            count_list[1] = tmp + 1
            time.sleep(2)
            print("Done. count_list[1]:%s" % count_list[1])
    
    
    def change():
        with lock:  # 大鎖
            change_0()
            time.sleep(0.001)
            change_1()
    
    
    def verify(sub):
        global count_list
        thread_list = []
        for i in range(5):
            t = threading.Thread(target=sub, args=())
            t.start()
            thread_list.append(t)
        for j in thread_list:
            j.join()
        print(count_list)
    
    
    if __name__ == "__main__":
        verify(change)

第二種死鎖:互相等待死鎖

死鎖的另外一個原因是兩個進程想要獲得的鎖已經被對方進程獲得,只能互相等待又無法釋放已經獲得的鎖,而導致死鎖。假設銀行系統中,用戶a試圖轉賬100塊給用戶b,與此同時用戶b試圖轉賬500塊給用戶a,則可能產生死鎖。
2個線程互相等待對方的鎖,互相佔用着資源不釋放。

下面是一個互相調用導致死鎖的例子:

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 互相等待死鎖.py
    @time: 2020/3/3 22:29
    @author:LDC
    '''
    import threading
    import time
    
    
    class Account(object):
        def __init__(self, name, balance, lock):
            self.name = name
            self.balance = balance
            self.lock = lock
    
        def withdraw(self, amount):
            # 轉賬
            self.balance -= amount
    
        def deposit(self, amount):
            # 存款
            self.balance += amount
    
    
    def transfer(from_account, to_account, amount):
        # 轉賬操作
        with from_account.lock:
            from_account.withdraw(amount)
            time.sleep(1)
            print("trying to get %s's lock..." % to_account.name)
            with to_account.lock:
                to_account.deposit(amount)
        print("transfer finish")
    
    
    if __name__ == "__main__":
        a = Account('a', 1000, threading.RLock())
        b = Account('b', 1000, threading.RLock())
        thread_list = []
        thread_list.append(threading.Thread(target=transfer, args=(a, b, 100)))
        thread_list.append(threading.Thread(target=transfer, args=(b, a, 500)))
        for i in thread_list:
            i.start()
        for j in thread_list:
            j.join()

最終結果是死鎖:

    trying to get account a's lock...
    trying to get account b's lock...

即我們的問題是:

你正在寫一個多線程程序,其中線程需要一次獲取多個鎖,此時如何避免死鎖問題。
解決方案:
在多線程程序中,死鎖問題很大一部分是由於線程同時獲取多個鎖造成的。

舉個例子:一個線程獲取了第一個鎖,然後在獲取第二個鎖的時候發生阻塞,那麼這個線程就可能阻塞其他線程的執行,從而導致整個程序假死。

其實解決這個問題,核心思想是:目前我們遇到的問題是兩個線程想獲取到的鎖,都被對方線程拿到了,那麼我們只需要保證在這兩個線程中,獲取鎖的順序保持一致就可以了。舉個例子,我們有線程thread_a, thread_b, 鎖lock_1, lock_2。只要我們規定好了鎖的使用順序,比如先用lock_1,再用lock_2,當線程thread_a獲得lock_1時,其他線程如thread_b就無法獲得lock_1這個鎖,也就無法進行下一步操作(獲得lock_2這個鎖),也就不會互相等待導致的死鎖。

簡言之,解決死鎖問題的一種方案是爲程序中的每一個鎖分配一個唯一的id,然後只允許按照升序規則來使用多個鎖,這個規則使用上下文管理器 是非常容易實現的,

升序鎖

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 升序鎖.py
    @time: 2020/3/3 22:39
    @author:LDC
    '''
    
    import threading
    import time
    from contextlib import contextmanager
    
    thread_local = threading.local()
    
    
    @contextmanager
    def acquire(*locks):
        # sort locks by object identifier
        # 根據對象標識符對鎖進行排序
        locks = sorted(locks, key=lambda x: id(x))
    
        # make sure lock order of previously acquired locks is not violated
        # 確保沒有違反先前獲取的鎖的順序
        acquired = getattr(thread_local, 'acquired', [])
        if acquired and (max(id(lock) for lock in acquired) >= id(locks[0])):
            raise RuntimeError('Lock Order Violation')
    
        # Acquire all the locks
        # 獲取所有鎖
        acquired.extend(locks)
        thread_local.acquired = acquired
    
        try:
            for lock in locks:
                lock.acquire()
            yield
        finally:
            for lock in reversed(locks):
                lock.release()
            del acquired[-len(locks):]
    
    
    class Account(object):
        def __init__(self, name, balance, lock):
            self.name = name
            self.balance = balance
            self.lock = lock
    
        def withdraw(self, amount):
            self.balance -= amount
    
        def deposit(self, amount):
            self.balance += amount
    
    
    def transfer(from_account, to_account, amount):
        print("%s transfer..." % amount)
        with acquire(from_account.lock, to_account.lock):
            from_account.withdraw(amount)
            time.sleep(1)
            to_account.deposit(amount)
        print("%s transfer... %s:%s ,%s: %s" % (
        amount, from_account.name, from_account.balance, to_account.name, to_account.balance))
        print("transfer finish")
    
    
    if __name__ == "__main__":
        a = Account('a', 1000, threading.Lock())
        b = Account('b', 1000, threading.Lock())
    
        thread_list = []
        thread_list.append(threading.Thread(target=transfer, args=(a, b, 100)))
        thread_list.append(threading.Thread(target=transfer, args=(b, a, 500)))
        for i in thread_list:
            i.start()
        for j in thread_list:
            j.join()

運行結果:

    transfer...
    transfer...
    transfer... a:900 ,b:1100
    transfer finish
    transfer... b:600, a:1400
    transfer finish

成功的避免了互相等待導致的死鎖問題。

在上述代碼中,有幾點語法需要解釋:

    1. 裝飾器@contextmanager是用來讓我們能用with語句調用鎖的,從而簡化鎖的獲取和釋放過程。關於with語句,大家可以參考淺談 Python 的 with 語句(https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/)。簡言之,with語句在調用時,先執行 enter()方法,然後執行with結構體內的語句,最後執行exit()語句。有了裝飾器@contextmanager. 生成器函數中 yield 之前的語句在 enter() 方法中執行,yield 之後的語句在 exit() 中執行,而 yield 產生的值賦給了 as 子句中的 value 變量。
    1. try和finally語句中實現的是鎖的獲取和釋放。
    1. try之前的語句,實現的是對鎖的排序,以及鎖排序是否被破壞的判斷。

線程同步(Event)

線程之間經常需要協同工作,通過某種技術,讓一個線程訪問某些數據時,其它線程不能訪問這些數據,直到該線程完成對數據的操作。這些技術包括臨界區(Critical Section)互斥量(Mutex)信號量(Semaphore)事件Event等。

互斥鎖、遞歸鎖、升序鎖是實現線程同步的一個方法,其它的還有semaphore 信號量機制,event 事件機制。

semaphore 信號量機制在python 裏面也很簡單就能夠實現線程的同步。

如果對操作系統有一定的瞭解, 那麼對操作系統的PV原語操作應該有印象, 信號量其實就是基於這個機制的.

semaphore 類是threading 模塊下的一個類, 主要兩個函數: acquire 函數, release 函數這和lock 類的函數是一樣的, 只不過功能不一樣, semaphore 機制的acquire 函數的參數允許你自己設置最大的併發量, 就是說允許多少個線程來操作同一個函數或是變量, 同時執行一次就會遞減一次, release 函數則是遞增, 如果計數到了0, 則阻塞起線程, 不再允許線程訪問該方法或是變量.

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 信號量實現線程同步.py
    @time: 2020/3/4 22:30
    @author:LDC
    '''
    # python 多線程同步   semaphore
    import threading
    import time
    
    # 初始化信號量數量...當調用acquire 將數量置爲 0, 將阻塞線程等待其他線程調用release() 函數
    semaphore = threading.Semaphore(2)
    
    
    def func():
        if semaphore.acquire():
            for i in range(2):
                print(threading.currentThread().getName() + ' get semaphore')
                time.sleep(1)
            semaphore.release()
            print(threading.currentThread().getName() + ' release semaphore')
    
    
    if __name__ == '__main__':
        for i in range(4):
            t1 = threading.Thread(target=func)
            t1.start()
    

輸出

    Thread-1 get semaphore
    Thread-2 get semaphore
    Thread-1 get semaphore
    Thread-2 get semaphore
    Thread-1 release semaphore
    Thread-3 get semaphore
    Thread-2 release semaphore
    Thread-4 get semaphore
    Thread-3 get semaphore
    Thread-4 get semaphore
    Thread-3 release semaphore
    Thread-4 release semaphore

可以看到主體函數一次只允許兩個線程訪問。

event 機制不僅能夠實現線程間的通信, 也是實現線程同步的一個好方法。

事件是線程之間通信的最簡單的機制之一, 一個線程指示一個事件和其他線程等待它.
event.py 是threading 模塊下的一個類,

相比較前面兩個機制, 這個類提供了四個方法, 分別是:

is_set() 函數, set() 函數, clear() 函數, wait() 函數. threading庫中的event對象通過使用內部一個flag標記,通過flag的True或者False的變化來進行操作。

名稱 含義
set( ) 標記設置爲True
clear( ) 標記設置爲False
is_set( ) 標記是否爲True
wait(timeout=None) 設置等待標記爲True的時長,None爲無限等待。等到返回True,等不到返回False
    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: 事件機制實現線程同步.py
    @time: 2020/3/4 22:46
    @author:LDC
    '''
    
    import logging
    import threading
    import time
    
    
    # 打印線程名以及日誌信息
    logging.basicConfig(level=logging.DEBUG, format="(%(threadName)-10s : %(message)s", )
    
    
    def wait_for_event_timeout(e, t):
        """Wait t seconds and then timeout"""
        while not e.isSet():
            logging.debug("wait_for_event_timeout starting")
            event_is_set = e.wait(t)  # 阻塞, 等待設置爲true
            logging.debug("event set: %s" % event_is_set)
            if event_is_set:
                logging.debug("processing event")
            else:
                logging.debug("doing other work")
    
    
    e = threading.Event()  # 初始化爲false
    t2 = threading.Thread(name="nonblock", target=wait_for_event_timeout, args=(e, 2))
    t2.start()
    logging.debug("Waiting before calling Event.set()")
    # time.sleep(7)
    e.set()  # 喚醒線程, 同時將event 設置爲true
    logging.debug("Event is set")

輸出:
在這裏插入圖片描述

協程

協程,是單線程下的併發,又稱微線程,英文名Coroutine。是一種用戶態的輕量級線程,即協程是由用戶程序自己控制調度的。協程能保留上一次調用時的狀態,每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置,當程序中存在大量不需要CPU的操作時(IO),適用於協程。【在一個線程中CPU來回切換執行不同的任務,這種現象就是協程】

協程有極高的執行效率,因爲子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷。

不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。

因爲協程是一個線程執行,所以想要利用多核CPU,最簡單的方法是多進程+協程,這樣既充分利用多核,又充分發揮協程的高效率。

那符合什麼條件就能稱之爲協程:

1、必須在只有一個單線程裏實現併發

2、修改共享數據不需加鎖

3、用戶程序裏自己保存多個控制流的上下文棧

4、一個協程遇到IO操作自動切換到其它協程

python中對於協程有四個模塊,greenlet、gevent、yield和async來實現切換+保存線程

通過yield來實現任務切換+保存線程

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: yield_test.py
    @time: 2020/3/5 15:39
    @author:LDC
    '''
    import time
    
    
    def func1():
        while True:
            print('func1')
            yield '返回func1'
    
    
    def func2():
        g = func1()
        print(next(g))
        for i in range(3):
            print(next(g))
            time.sleep(3)
            print('func2')
    
    
    if __name__ == '__main__':
        start = time.time()
        func2()
        stop = time.time()
        print(stop - start)
    

yield不能節省IO時間,只是單純的進行程序切換

    # 基於yield併發執行,多任務之間來回切換,這就是個簡單的協程的體現,但是他能夠節省I/O時間嗎?不能
    import time
    
    
    def consumer():
        '''任務1:接收數據,處理數據'''
        while True:
            x = yield
            time.sleep(1)  # 發現什麼?只是進行了切換,但是並沒有節省I/O時間
            print('處理了數據:', x)
    
    
    def producer():
        '''任務2:生產數據'''
        g = consumer()
        next(g)  # 找到了consumer函數的yield位置
        for i in range(3):
            g.send(i)  # 給yield傳值,然後再循環給下一個yield傳值,並且多了切換的程序,比直接串行執行還多了一些步驟,導致執行效率反而更低了。
            print('發送了數據:', i)
    
    
    if __name__ == '__main__':
        start = time.time()
        # 基於yield保存狀態,實現兩個任務直接來回切換,即併發的效果
        # PS:如果每個任務中都加上打印,那麼明顯地看到兩個任務的打印是你一次我一次,即併發執行的.
        producer()  # 我在當前線程中只執行了這個函數,但是通過這個函數裏面的send切換了另外一個任務
        stop = time.time()
        print(stop - start)
        # 串行執行的方式
        # start = time.time()
        # res = producer()
        # consumer()
        # stop = time.time()
        # print(stop - start)
    

yield檢測不到IO,無法實現遇到IO自動切換。

greenlet是手動切換

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: greenlet創建協程.py
    @time: 2020/3/4 23:08
    @author:LDC
    '''
    
    '''
    使用greenlet + switch實現協程調度
    '''
    from greenlet import greenlet
    import time
    
    
    def func1():
        print("開門走進衛生間")
        time.sleep(3)
        gr2.switch()  # 把CPU執行權交給gr2
    
        print("飛流直下三千尺")
        time.sleep(3)
        gr2.switch()
        pass
    
    
    def func2():
        print("一看拖把放旁邊")
        time.sleep(3)
        gr1.switch()
    
        print("疑是銀河落九天")
        pass
    
    
    if __name__ == '__main__':
        gr1 = greenlet(func1)
        gr2 = greenlet(func2)
        gr1.switch()  # 把CPU執行權先給gr1
        pass
    

greenlet只是提供了一種比yield(生成器)更加便捷的切換方式,當切到一個任務執行時如果遇到IO,那就原地阻塞,仍然是沒有解決遇到IO自動切換來提升效率的問題。

Gevent實現自動切換協程(多協程

協程的本質就是在單線程下,由用戶自己控制一個任務遇到io阻塞了就切換另外一個任務去執行,以此來提升效率。

一般在工作中我們都是進程+線程+協程的方式來實現併發,以達到最好的併發效果。

如果是4核的CPU,一般起5個進程,每個進程中20個線程(5倍CPU數量),每個線程可以起500個協程,大規模爬取頁面的時候,等待網絡延遲的時間的時候,我們就可以用協程去實現併發。併發數量=520500從而達到5000個併發,這是一般一個4個CPU的機器最大的併發數。nginx在負載均衡的時候最大承載量是5w個。

單線程裏的這20個任務的代碼通常既有計算操作又有阻塞操作,我們完全可以在執行任務1時遇到阻塞,就利用阻塞的時間去執行任務2。。。如此,才能提高效率,這就用到了Gevent模塊。

Gevent(自動切換,由於切換是在IO操作時自動完成,所以gevent需要修改Python自帶的一些標準庫,這一過程在啓動時通過monkey patch完成)。

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: monkey_patch.py
    @time: 2020/3/4 23:40
    @author:LDC
    '''
    
    '''
    使用gevent + monkey.patch_all()自動調度網絡IO協程
    '''
    from gevent import monkey;
    
    monkey.patch_all()  # 將【標準庫-阻塞IO實現】替換爲【gevent-非阻塞IO實現,即遇到需要等待的IO會自動切換到其它協程
    import sys
    import gevent
    import requests
    import time
    
    sys.setrecursionlimit(1000000)  # 增加遞歸深度
    
    
    def get_page_text(url, order):
        print('No{}請求'.format(order))
        try:
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
            }
            resp = requests.get(url, headers=headers)  # 發起網絡請求,返回需要時間——阻塞IO
    
            html = resp.text
            html_len = len(html)
            print("%s成功返回:長度爲%d" % (url, html_len))
            return html_len
        except Exception as e:
            print('{}發生錯誤,{}'.format(url, e))
            return 0
    
    
    def gevent_joinall():
        # spawn是異步提交任務
        gevent.joinall([
            gevent.spawn(get_page_text, "http://www.sina.com", order=1),
            gevent.spawn(get_page_text, "http://www.qq.com", order=2),
            gevent.spawn(get_page_text, "http://www.baidu.com", order=3),
            gevent.spawn(get_page_text, "http://www.163.com", order=4),
            gevent.spawn(get_page_text, "http://www.4399.com", order=5),
            gevent.spawn(get_page_text, "http://www.sohu.com", order=6),
            gevent.spawn(get_page_text, "http://www.youku.com", order=7),
        ])
        g_iqiyi = gevent.spawn(get_page_text, "http://www.iqiyi.com", order=8)
        g_iqiyi.join()
        # #拿到任務的返回值
        print('獲取返回值', g_iqiyi.value)
    
    
    if __name__ == '__main__':
        #
        start = time.time()
        time.clock()
        gevent_joinall()
        end = time.time()
        print("over,耗時%d秒" % (end - start))
        print(time.clock())

monkey.patch_all() 一定要放到導入requests模塊之前,否則gevent無法識別requests的阻塞。

async實現協程

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: async創建協程.py
    @time: 2020/3/6 12:03
    @author:LDC
    '''
    
    import asyncio
    from functools import partial
    
    '''
    使用gevent + monkey.patch_all()自動調度網絡IO協程
    '''
    import sys
    import requests
    import time
    
    sys.setrecursionlimit(1000000)  # 增加遞歸深度
    
    
    async def get_page_text(url, order):
        # 使用async創建一個可中斷的異步函數
        print('No{}請求'.format(order))
        try:
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
            }
            # 利用BaseEventLoop.run_in_executor()可以在coroutine中執行第三方的命令,例如requests.get()
            # 第三方命令的參數與關鍵字利用functools.partial傳入
            future = asyncio.get_event_loop().run_in_executor(None, partial(requests.get, url, headers=headers))
            resp = await future
    
            html = resp.text
            html_len = len(html)
            print("%s成功返回:長度爲%d" % (url, html_len))
            return html_len
        except Exception as e:
            print('{}發生錯誤,{}'.format(url, e))
            return 0
    
    
    # 異步函數執行完後回調函數
    def callback(future):  # 這裏默認傳入一個future對象
        print(future.result())
    
    
    # 異步函數執行完後回調函數,可接收多個參數
    def callback_2(url, future):  # 傳入值的時候,future必須在最後一個
        print(url, future.result())
    
    
    def async_run():
        # 使用async創建協程
        urls = ["http://www.youku.com", "http://www.sina.com", "http://www.qq.com", "http://www.baidu.com",
                "http://www.163.com", "http://www.4399.com", "http://www.sohu.com",
    
                ]
        loop_task = []
        loop = asyncio.get_event_loop()
        for i in range(len(urls)):
            t = asyncio.ensure_future(get_page_text(urls[i], i + 1))
            # t = loop.create_task(task(urls[i], i + 1))
            t.add_done_callback(callback)
            # t.add_done_callback(partial(callback_2, urls[i]))
            loop_task.append(t)
        print('等待所有async函數執行完成')
        start = time.time()
        loop.run_until_complete(asyncio.wait(loop_task))
        loop.close()
        end = time.time()
        print("over,耗時%d秒" % (end - start))
    
    
    if __name__ == '__main__':
        async_run()

3、關鍵字:yield

3.1 yield表達式

yield相當於return,只不過return是終結函數並返回一個值,而yield是先把值返回並把函數掛起來,以後還會執行yield以下的語句。

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: yield_test.py
    @time: 2020/3/5 15:39
    @author:LDC
    '''
    
    
    def foo(end_count):
        print('yield生成器')
        count = 0
        while count < end_count:
            res = yield count
            print('接收到的參數', res)
            if res is not None:
                count = res
            else:
                count += 1
    
    
    if __name__ == '__main__':
        f = foo(7)
        # 第一次調用yield函數是預激活,
        # 即調用函數foo時,只執行yield前面的語句,
        # 遇到yield就把foo函數掛起來,
        # 並返回yield後面附帶的值
        print(next(f))  # 使用next()調用yield函數
        for i in range(3):
            # 第二次調用就開始執行yield後面的語句
            print(next(f))  # 使用next()調用yield函數
        print('*' * 20)
        # s使用send給foo函數傳值,yield會接收到並賦給res,
        print(f.send(5))  # send函數中會執行一次next函數
        print(next(f))
    
        # 生成不佔空間的列表[0,1,2,3,4,5,6,7,8,9]
        # for i in foo(10):
        #     print(i)

輸出:

    yield生成器
    0
    接收到的參數 None
    1
    接收到的參數 None
    2
    接收到的參數 None
    3
    ********************
    接收到的參數 5
    5
    接收到的參數 None
    6

1、第一次ti調用next函數時,進入foo函數,遇到yield就把count=0返回,並把foo函數掛起

2、在for循環中再次調用next函數時,就開始執行yield後面的賦值語句,由於沒有接收到值就默認爲None,所以res=None

3、然後接着執行賦值語句後面的打印語句和if判斷,由於res爲None所以執行count +=1,此時count值爲1

4、再次遇到yield,返回1,並把foo函數掛起。

5、send函數是可以給yield生成器傳參的,執行send函數時會默認執行一次next函數,原理同上。

到這裏你可能就明白yield和return的關係和區別了,帶yield的函數是一個生成器,而不是一個函數了,這個生成器有一個函數就是next函數,next就相當於“下一步”生成哪個數,這一次的next開始的地方是接着上一次的next停止的地方執行的,所以調用next的時候,生成器並不會從foo函數的開始執行,只是接着上一步停止的地方開始,然後遇到yield後,return出要生成的數,此步就結束。

爲什麼用這個生成器,是因爲如果用List的話,會佔用更大的空間,比如說取0,1,2,3,4,5,6…1000

你可能會這樣:

    for i in range(1000):
        print(i)

這個時候range(1000)就默認生成一個含有1000個數的list了,所以很佔內存。

這個時候你可以用剛纔的yield組合成生成器進行實現

    for i in foo(10000):
        print(i)

但這個由於每次都要調用函數foo,所以比較耗時間。【這就是用時間換空間】

4、關鍵字:async/await

asyncio 是用來編寫 併發 代碼的庫,使用 async/await 語法。

asyncio 被用作多個提供高性能 Python 異步框架的基礎,包括網絡和網站服務,數據庫連接庫,分佈式任務隊列等等。

asyncio 往往是構建 IO 密集型和高層級 結構化 網絡代碼的最佳選擇。

正常的函數在執行時是不會中斷的,所以你要寫一個能夠中斷的函數,就需要添加async關鍵。

async 用來聲明一個函數爲異步函數,異步函數的特點是能在函數執行過程中掛起,去執行其他異步函數,等到掛起條件(假設掛起條件是asyncio.sleep(5))消失後,也就是5秒到了再回來執行。

await 用來用來聲明程序掛起,比如異步程序執行到某一步時需要等待的時間很長,就將此掛起,去執行其他的異步程序。await 後面只能跟異步程序或有await屬性的對象,因爲異步程序與一般程序不同。假設有兩個異步函數async a,async b,a中的某一步有await,當程序碰到關鍵字await b()後,異步程序掛起後去執行另一個異步b程序,就是從函數內部跳出去執行其他函數,當掛起條件消失後,不管b是否執行完,要馬上從b程序中跳出來,回到原程序執行原來的操作。如果await後面跟的b函數不是異步函數,那麼操作就只能等b執行完再返回,無法在b執行的過程中返回。如果要在b執行完才返回,也就不需要用await關鍵字了,直接調用b函數就行。所以這就需要await後面跟的是異步函數了。在一個異步函數中,可以不止一次掛起,也就是可以用多個await。

可以使用async、await來實現協程的併發,下面以一個爬蟲例子來說明:

    # encoding: utf-8
    '''
    @contact: [email protected]
    @wechat: 1257309054
    @Software: PyCharm
    @file: async_await.py
    @time: 2020/3/5 23:15
    @author:LDC
    '''
    from gevent import monkey;
    
    monkey.patch_all()
    import gevent
    
    import asyncio
    from functools import wraps, partial
    import time
    
    import requests
    
    
    # 定義一個查看函數執行時間的裝飾器
    def func_use_time(func):
        @wraps(func)
        def inside(*arg, **kwargs):
            start = time.clock()
            res = func(*arg, **kwargs)
            print('***************執行時間*****************', time.clock() - start)
            return res
    
        return inside
    
    
    def get_page_text(url):
        # 爬取網站
        print(url)
        try:
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
            }
            resp = requests.get(url, headers=headers)  # 發起網絡請求,返回需要時間——阻塞IO
    
            html = resp.text
            return html
        except Exception as e:
            print('{}發生錯誤,{}'.format(url, e))
            return ''
    
    
    class Narmal():
        # 正常爬取
        def __init__(self, urls):
            self.urls = urls
            self.res_dict = {}
    
        @func_use_time
        def run(self):
            for url in self.urls:
                res = get_page_text(url)
                self.res_dict[url] = len(res)
            print('串行獲取結果', self.res_dict)
    
    
    class UseAsyncio():
        # 使用async實現協程併發
        def __init__(self, urls):
            self.urls = urls
            self.res_dict = {}
    
        # 定義一個異步函數,執行爬取任務
        async def task(self, url):
            print(url)
            try:
                headers = {
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
                }
                # 利用BaseEventLoop.run_in_executor()可以在coroutine中執行第三方的命令,例如requests.get()
                # 第三方命令的參數與關鍵字利用functools.partial傳入
                future = asyncio.get_event_loop().run_in_executor(None, partial(requests.get, url, headers=headers))
                resp = await future
                html = resp.text
                self.res_dict[url] = len(html)
                return html
            except Exception as e:
                print('{}發生錯誤,{}'.format(url, e))
                return ''
    
        @func_use_time
        def run(self):
            loop = asyncio.get_event_loop()
            tasks = [asyncio.ensure_future(self.task(url)) for url in self.urls]
            loop.run_until_complete(asyncio.wait(tasks))
            loop.close()
            # 獲取async結果
            # for task in tasks:
            #     print(task.result())
            print('async獲取結果', self.res_dict)
    
    
    class UseGevent():
        # 使用Gevent實現協程併發
        def __init__(self, urls):
            self.urls = urls
            self.res_dict = {}
    
        def task(self, url):
            res = get_page_text(url)
            self.res_dict[url] = len(res)
    
        @func_use_time
        def run(self):
            gevent.joinall([gevent.spawn(self.task, url) for url in self.urls])
            print(self.res_dict)
    
    
    if __name__ == '__main__':
        urls = ["http://www.sina.com", "http://www.qq.com", "http://www.baidu.com",
                "http://www.163.com", "http://www.4399.com", "http://www.sohu.com",
                "http://www.youku.com",
                ]
        print("使用正常爬取方式,即串行")
        Narmal(urls).run()
        print("使用Asyncio爬取方式,async實現協程併發")
        UseAsyncio(urls).run()
        print("使用Gevent爬取方式,實現協程併發")
        UseGevent(urls).run()
    

輸出:

    使用正常爬取方式,即串行
    http://www.sina.com
    http://www.qq.com
    http://www.baidu.com
    http://www.163.com
    http://www.4399.com
    http://www.sohu.com
    http://www.youku.com
    串行獲取結果 {'http://www.sina.com': 539723, 'http://www.qq.com': 227753, 'http://www.baidu.com': 166916, 'http://www.163.com': 483531, 'http://www.4399.com': 172837, 'http://www.sohu.com': 178312, 'http://www.youku.com': 990760}
    ***************執行時間***************** 1.9352532
    使用Asyncio爬取方式,async實現協程併發
    http://www.sina.com
    http://www.qq.com
    http://www.baidu.com
    http://www.163.com
    http://www.4399.com
    http://www.sohu.com
    http://www.youku.com
    async獲取結果 {'http://www.4399.com': 172837, 'http://www.163.com': 483531, 'http://www.qq.com': 227753, 'http://www.sohu.com': 178310, 'http://www.baidu.com': 166625, 'http://www.sina.com': 539723, 'http://www.youku.com': 1047892}
    ***************執行時間***************** 0.951011
    使用Gevent爬取方式,實現協程併發
    http://www.sina.com
    http://www.qq.com
    http://www.baidu.com
    http://www.163.com
    http://www.4399.com
    http://www.sohu.com
    http://www.youku.com
    {'http://www.163.com': 483531, 'http://www.4399.com': 172837, 'http://www.sohu.com': 178312, 'http://www.qq.com': 227753, 'http://www.baidu.com': 166760, 'http://www.sina.com': 539723, 'http://www.youku.com': 994926}
    ***************執行時間***************** 1.0057508

相對來說還是使用async執行效率高些

後記

【後記】爲了讓大家能夠輕鬆學編程,我創建了一個公衆號【輕鬆學編程】,裏面有讓你快速學會編程的文章,當然也有一些乾貨提高你的編程水平,也有一些編程項目適合做一些課程設計等課題。

也可加我微信【1257309054】,拉你進羣,大家一起交流學習。
如果文章對您有幫助,請我喝杯咖啡吧!

公衆號

公衆號

讚賞碼

關注我,我們一起成長~~

參考文章

【https://www.cnblogs.com/ArsenalfanInECNU/p/10022740.html】

【https://www.cnblogs.com/windyrainy/p/10647598.html】

【https://blog.csdn.net/mr__l1u/article/details/81772073】

【https://www.cnblogs.com/melonjiang/p/5307705.html】

【https://www.cnblogs.com/huangguifeng/p/7632799.html】

【https://blog.csdn.net/lm_is_dc/article/details/80960185】

【https://blog.csdn.net/lm_is_dc/article/details/80959734】

【https://blog.csdn.net/lm_is_dc/article/details/80959911】

【https://www.cnblogs.com/russellyoung/p/python-zhi-xie-cheng.html】

【https://blog.csdn.net/mieleizhi0522/article/details/82142856/】

【https://docs.python.org/zh-cn/3/library/asyncio.html】

【https://www.cnblogs.com/xinghun85/p/9937741.html】

【https://www.cnblogs.com/dhcn/p/9032461.html】

【https://www.cnblogs.com/callyblog/p/11216961.html】

【https://www.cnblogs.com/daofaziran/p/10154986.html】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章