Python學習筆記 - 多程式

MADAO是不會開花的發表於2019-01-22
  • 什麼是GIL

    全域性直譯器鎖(英語:Global Interpreter Lock,縮寫GIL),是計算機程式設計語言直譯器用於同步執行緒的一種機制,它使得任何時刻僅有一個執行緒在執行。即便在多核心處理器上,使用 GIL 的直譯器也只允許同一時間執行一個執行緒。常見的使用 GIL 的直譯器有CPython與Ruby MRI。 -- 維基百科

    一般用的Python的直譯器就是CPython(官網下載的版本),結合上面對GIL的解釋,得出兩個結論:

    1. Python中的多執行緒並不是同時有多個執行緒並行執行,而是不斷的切換執行緒達到多執行緒並行的效果,同一時間只能執行一個執行緒。

    2. Python的多執行緒程式並不能利用多核CPU的優勢,無論有多少個執行緒,只能執行在一個核上。

  • 測試GIL

    import threading
    import time
    from queue import Queue
    
    
    def normal(num_list):
        total = sum(num_list)
        print(total)
    
    
    def mutilThread_sum(l, q):
        q.put(sum(l))
    
    
    def mutilThread(num_list):
        q = Queue()
        threads = []
        total = 0
        for idx in range(4):
            thread = threading.Thread(target=mutilThread_sum, args=(num_list[:], q))
            thread.start()
            threads.append(thread)
        [thread.join() for thread in threads]
        for thread in threads:
            total += q.get()
        print(total)
    
    
    num_list = list(range(1000000))
    
    normal_start = time.time()
    normal(num_list*4)
    print('normal:', time.time() - normal_start)
    
    mutilThread_start = time.time()
    mutilThread(num_list)
    print('mutilThread:', time.time() - mutilThread_start)
    
    
    複製程式碼

    結果:

    Python學習筆記 - 多程式

    單執行緒計算的是多執行緒中每個執行緒計算的4倍,理論上多執行緒應該比單執行緒快3~4倍左右,但是從結果看來mutilThread並沒有比normal快那麼多,而且如果多執行幾次你還會發現多執行緒有時候還會比單執行緒慢。這就是因為GIL的存在導致的。

  • 多程式

    雖然無法使用多執行緒利用多核CPU的優勢,但是可以使用多程式達到這一目的。

    • multiprocessing

      Python中多程式需要藉助multiprocessing模組實現。這個模組和Threading模組很像。

      from multiprocessing import Process
      
      
      def add_one(a):
          print(a + 1)
      
      
      p1 = Process(target=add_one, args=(1,)) # 建立一個程式
      p1.start()  # 啟動程式
      p1.join()   # 等待程式結束
      複製程式碼
    • 程式中函式的輸出

      觀察上面例子中的函式,如果把print(a + 1)換為return a + 1。那麼怎樣才能拿到這個函式的返回值?用佇列。

      from multiprocessing import Process, Queue
      
      
      def add_one(q, a):
          q.put(a + 1)
      
      
      q = Queue()
      p1 = Process(target=add_one, args=(q, 1))
      p2 = Process(target=add_one, args=(q, 10))
      p1.start()
      p2.start()
      p1.join()
      p2.join()
      
      a = q.get()
      b = q.get()
      
      print(a + b)  # 13
      複製程式碼
  • 簡單的多程式,多執行緒效率對比

    from multiprocessing import Process, Queue
    from threading import Thread
    import time
    
    
    def calculate(q):
        res = 0
        for i in range(10000000):
            res += i ** 2
        q.put(res)
    
    
    # 多程式
    def multi_process():
        q = Queue()
        p1 = Process(target=calculate, args=(q,))
        p2 = Process(target=calculate, args=(q,))
        p1.start()
        p2.start()
        p1.join()
        p2.join()
    
        res = q.get() + q.get()
        print('multi_process:', res)
    
    
    # 多執行緒
    def multi_thread():
        q = Queue()
        p1 = Thread(target=calculate, args=(q,))
        p2 = Thread(target=calculate, args=(q,))
        p1.start()
        p2.start()
        p1.join()
        p2.join()
    
        res = q.get() + q.get()
        print('multi_thread:', res)
    
    
    # 單執行緒
    def normal():
        res = 0
        for i in range(2):
            for j in range(10000000):
                res += j ** 2
        print('normal:', res)
    
    
    t1 = time.time()
    normal()
    t2 = time.time()
    print('normal time:', t2 - t1)
    multi_process()
    t3 = time.time()
    print('multi_process time:', t3 - t2)
    multi_thread()
    t4 = time.time()
    print('multi_thread time:', t4 - t3)
    
    複製程式碼

    結果:

    Python學習筆記 - 多程式

    對比一下,多程式效率是最高的,當然了這個例子是屬於計算密集型的例子,在io密集型的程式上,多執行緒的優勢還是挺大的。

  • 程式池

    如果需要大量的建立程式,那麼就可以使用程式池這個功能:Pool

    from multiprocessing import Pool
    from os import getpid
    import time
    import random
    
    
    def square(num):
        print('程式id:%s啟動' % getpid())
        time.sleep(random.random() * 3)
        print('程式id:%s執行完畢' % getpid())
        return num ** 2
    
    
    p = Pool()
    results = []
    for i in range(10):
        res = p.apply_async(square, args=(i,))
        results.append(res)
    
    p.close()
    p.join()
    
    for result in results:
        print(result.get())
    
    複製程式碼

    結果:

    Python學習筆記 - 多程式

    解釋:

    1. pool:用於建立一個程式池物件,可以傳入同時跑的程式的數量,預設的程式數是cpu的核數。比如:

      p = Pool(processes=10)
      複製程式碼

      這樣就可以讓10個程式同時跑,仔細觀察上面例子的結果,前8個程式是立即就啟動了,因為沒有給pool方法傳入程式數量,預設就是cpu核的數量。我這裡是8,所以同時只能跑8個程式,當這八個程式中的某一個程式的任務執行完成後才會啟動新的程式。

    2. apply_async:啟動程式,執行引數中傳入的函式,返回的結果使用get方法獲取,注意get是會阻塞的,也就是說如果寫成下面這樣的話,就變成了一個程式執行完成再啟動下一個程式,和單程式沒什麼兩樣了:

      for i in range(10):
          res = p.apply_async(square, args=(i,))
          res.get()
      複製程式碼
    3. close:就是關閉pool,不再接受新的程式。

    4. join:等待所有的子程式執行完畢,在呼叫join之前要先呼叫close或者terminate方法,terminate方法是結束工作的程式,不管任務是否執行完成。

    如果不喜歡apply_async這種,還可以使用map,例子:

    from multiprocessing import Pool
    from os import getpid
    import time
    import random
    
    
    def square(num):
        print('程式id:%s啟動' % getpid())
        time.sleep(random.random() * 3)
        print('程式id:%s執行完畢' % getpid())
        return num ** 2
    
    
    p = Pool()
    res = p.map(square, range(10))
    
    p.close()
    p.join()
    
    print(res)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    複製程式碼

相關文章