進程和線程

最近會開始繼續 Python 的進階系列文章,這是該系列的第一篇文章,介紹進程和線程的知識,恰好上一篇文章就介紹了採用 concurrent.futures 模塊實現多進程和多線程的操做,本文則介紹下進程和線程的概念,多進程和多線程各自的實現方法和優缺點,以及分別在哪些狀況採用多進程,或者是多線程。html

概念

併發編程就是實現讓程序同時執行多個任務,而如何實現併發編程呢,這裏就涉及到進程線程這兩個概念。python

對於操做系統來講,一個任務(或者程序)就是一個進程(Process),好比打開一個瀏覽器是開啓一個瀏覽器進程,打開微信就啓動了一個微信的進程,打開兩個記事本,就啓動兩個記事本進程。git

進程的特色有:github

  • 操做系統以進程爲單位分配存儲空間, 每一個進程有本身的地址空間、數據棧以及其餘用於跟蹤進程執行的輔助數據;
  • 進程能夠經過 fork 或者 spawn 方式建立新的進程來執行其餘任務
  • 進程都有本身獨立的內存空間,因此進程須要經過進程間通訊機制(IPC,Inter-Process Communication)來實現數據共享,具體的方式包括管道、信號、套接字、共享內存區

一個進程還能夠同時作多件事情,好比在 Word 裏面同時進行打字、拼音檢查、打印等事情,也就是一個任務分爲多個子任務同時進行,這些進程內的子任務被稱爲線程(Thread)算法

由於每一個進程至少須要完成一件事情,也就是一個進程至少有一個線程。當要實現併發編程,也就是同時執行多任務時,有如下三種解決方案:數據庫

  • 多進程,每一個進程只有一個線程,但多個進程一塊兒執行多個任務;
  • 多線程,只啓動一個進程,但一個進程內開啓多個線程;
  • 多進程+多線程,即啓動多個進程,每一個進程又啓動多個線程,但這種方法很是複雜,實際不多使用

注意:真正的並行執行多任務只有在多核 CPU 上才能夠實現,單核 CPU 系統中,真正的併發是不可能的,由於在某個時刻可以得到CPU的只有惟一的一個線程,多個線程共享了CPU的執行時間編程

Python 是同時支持多進程和多線程的,下面就分別介紹多進程和多線程。windows

多進程

Unix/Linux 系統中,提供了一個 fork() 系統調用,它是一個特殊的函數,普通函數調用是調用一次,返回一次fork 函數調用一次,返回兩次,由於調用該函數的是父進程,而後複製出一份子進程了,最後同時在父進程和子進程內返回,因此會返回兩次。瀏覽器

子進程返回的永遠是 0 ,而父進程會返回子進程的 ID,由於父進程能夠複製多個子進程,因此須要記錄每一個子進程的 ID,而子進程能夠經過調用 getpid() 獲取父進程的 ID。bash

Python 中 os 模塊封裝了常見的系統調用,這就包括了 fork ,代碼示例以下:

import os

print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
    print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
複製代碼

運行結果:

Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.
複製代碼

因爲 windows 系統中是不存在 fork ,因此上述函數沒法調用,但 Python 是跨平臺的,因此也仍是有其餘模塊能夠實現多進程的功能,好比 multiprocessing 模塊。

multiprocess

multiprocessing 模塊中提供了 Process 類來表明一個進程對象,接下來用一個下載文件的例子來講明採用多進程和不用多進程的差異。

首先是不採用多進程的例子:

def download_task(filename):
    '''模擬下載文件'''
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))


def download_without_multiprocess():
    '''不採用多進程'''
    start = time()
    download_task('Python.pdf')
    download_task('nazha.mkv')
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))
if __name__ == '__main__':
    download_without_multiprocess()
複製代碼

運行結果以下,這裏用 randint 函數來隨機輸出當前下載文件的耗時,從結果看,程序運行時間等於兩個下載文件的任務時間總和。

開始下載Python.pdf...
Python.pdf下載完成! 耗費了9秒
開始下載nazha.mkv...
nazha.mkv下載完成! 耗費了9秒
總共耗費了18.00秒.
複製代碼

若是是採用多進程,例子以下所示:

def download_task(filename):
    '''模擬下載文件'''
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))
    
def download_multiprocess():
    '''採用多進程'''
    start = time()
    p1 = Process(target=download_task, args=('Python.pdf',))
    p1.start()
    p2 = Process(target=download_task, args=('nazha.mkv',))
    p2.start()
    p1.join()
    p2.join()
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))
if __name__ == '__main__':
    download_multiprocess()
複製代碼

這裏多進程例子中,咱們經過 Process 類建立了進程對象,經過 target 參數傳入一個函數表示進程須要執行的任務,args 是一個元組,表示傳遞給函數的參數,而後採用 start 來啓動進程,而 join 方法表示等待進程執行結束。

運行結果以下所示,耗時就不是兩個任務執行時間總和,速度上也是大大的提高了。

開始下載Python.pdf...
開始下載nazha.mkv...
Python.pdf下載完成! 耗費了5秒
nazha.mkv下載完成! 耗費了9秒
總共耗費了9.36秒.
複製代碼
Pool

上述例子是開啓了兩個進程,但若是須要開啓大量的子進程,上述代碼的寫法就不合適了,應該採用進程池的方式批量建立子進程,仍是用下載文件的例子,但執行下部分的代碼以下所示:

import os
from multiprocessing import Process, Pool
from random import randint
from time import time, sleep

def download_multiprocess_pool():
    '''採用多進程,並用 pool 管理進程池'''
    start = time()
    filenames = ['Python.pdf', 'nazha.mkv', 'something.mp4', 'lena.png', 'lol.avi']
    # 進程池
    p = Pool(5)
    for i in range(5):
        p.apply_async(download_task, args=(filenames[i], ))
    print('Waiting for all subprocesses done...')
    # 關閉進程池
    p.close()
    # 等待全部進程完成任務
    p.join()
    end = time()
    print('總共耗費了%.2f秒.' % (end - start))
if __name__ == '__main__':
    download_multiprocess_pool()
複製代碼

代碼中 Pool 對象先建立了 5 個進程,而後 apply_async 方法就是並行啓動進程執行任務了,調用 join() 方法以前必須先調用 close() ,close() 主要是關閉進程池,因此執行該方法後就不能再添加新的進程對象了。而後 join() 就是等待全部進程執行完任務。

運行結果以下所示:

Waiting for all subprocesses done...
開始下載Python.pdf...
開始下載nazha.mkv...
開始下載something.mp4...
開始下載lena.png...
開始下載lol.avi...
nazha.mkv下載完成! 耗費了5秒
lena.png下載完成! 耗費了6秒
something.mp4下載完成! 耗費了7秒
Python.pdf下載完成! 耗費了8秒
lol.avi下載完成! 耗費了9秒
總共耗費了9.80秒.
複製代碼
子進程

大多數狀況,子進程是一個外部進程,而非自身。在建立子進程後,咱們還須要控制子進程的輸入和輸出。

subprocess 模塊可讓咱們很好地開啓子進程以及管理子進程的輸入和輸出。

下面是演示如何用 Python 演示命令 nslookup www.python.org,代碼以下所示:

import subprocess

print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
複製代碼

運行結果:

$ nslookup www.python.org
Server:		192.168.19.4
Address:	192.168.19.4#53

Non-authoritative answer:
www.python.org	canonical name = python.map.fastly.net.
Name:	python.map.fastly.net
Address: 199.27.79.223

Exit code: 0
複製代碼

若是子進程須要輸入,能夠經過 communicate() 進行輸入,代碼以下所示:

import subprocess

print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
複製代碼

這段代碼就是執行命令 nslookup 時,輸入:

set q=mx
python.org
exit
複製代碼

運行結果:

$ nslookup
Server:		192.168.19.4
Address:	192.168.19.4#53

Non-authoritative answer:
python.org	mail exchanger = 50 mail.python.org.

Authoritative answers can be found from:
mail.python.org	internet address = 82.94.164.166
mail.python.org	has AAAA address 2001:888:2000:d::a6


Exit code: 0
複製代碼
進程間通訊

進程之間是須要通訊的,multiprocess 模塊中也提供了 QueuePipes 等多種方式來交換數據。

這裏以 Queue 爲例,在父進程建立兩個子進程,一個往 Queue 寫入數據,另外一個從 Queue 讀取數據。代碼以下:

import os
from multiprocessing import Process, Queue
import random
from time import time, sleep

# 寫數據進程執行的代碼:
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        sleep(random.random())


# 讀數據進程執行的代碼:
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)


def ipc_queue():
    ''' 採用 Queue 實現進程間通訊 :return: '''
    # 父進程建立Queue,並傳給各個子進程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 啓動子進程pw,寫入:
    pw.start()
    # 啓動子進程pr,讀取:
    pr.start()
    # 等待pw結束:
    pw.join()
    # pr進程裏是死循環,沒法等待其結束,只能強行終止:
    pr.terminate()


if __name__ == '__main__':
    ipc_queue()
複製代碼

運行結果以下所示:

Process to write: 24992
Put A to queue...
Process to read: 22836
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.
複製代碼

多線程

前面也提到了一個進程至少包含一個線程,其實進程就是由若干個線程組成的線程是操做系統直接支持的執行單元,所以高級語言一般都內置多線程的支持,Python 也不例外,並且 Python 的線程是真正的 Posix Thread ,而不是模擬出來的線程

多線程的運行有以下優勢:

  • 使用線程能夠把佔據長時間的程序中的任務放到後臺去處理
  • 用戶界面能夠更加吸引人,好比用戶點擊了一個按鈕去觸發某些事件的處理,能夠彈出一個進度條來顯示處理的進度。
  • 程序的運行速度可能加快。
  • 一些等待的任務實現上如用戶輸入、文件讀寫和網絡收發數據等,線程就比較有用了。在這種狀況下咱們能夠釋放一些珍貴的資源如內存佔用等等。

線程能夠分爲:

  • **內核線程:**由操做系統內核建立和撤銷。
  • **用戶線程:**不須要內核支持而在用戶程序中實現的線程。

Python 的標準庫提供了兩個模塊:_threadthreading,前者是低級模塊,後者是高級模塊,對 _thread 進行了封裝。大多數狀況只須要採用 threading 模塊便可,而且也推薦採用這個模塊。

這裏再次如下載文件做爲例子,用多線程的方式來實現一遍:

from random import randint
from threading import Thread, current_thread
from time import time, sleep


def download(filename):
    print('thread %s is running...' % current_thread().name)
    print('開始下載%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下載完成! 耗費了%d秒' % (filename, time_to_download))


def download_multi_threading():
    print('thread %s is running...' % current_thread().name)
    start = time()
    t1 = Thread(target=download, args=('Python.pdf',), name='subthread-1')
    t1.start()
    t2 = Thread(target=download, args=('nazha.mkv',), name='subthread-2')
    t2.start()
    t1.join()
    t2.join()
    end = time()
    print('總共耗費了%.3f秒' % (end - start))
    print('thread %s is running...' % current_thread().name)


if __name__ == '__main__':
    download_multi_threading()
複製代碼

實現多線程的方式和多進程相似,也是經過 Thread 類建立線程對象,target 參數表示傳入須要執行的函數,args 參數是表示傳給函數的參數,而後 name 是給當前線程進行命名,默認命名是如 Thread- 一、Thread-2 等等。

此外,任何進程默認會啓動一個線程,咱們將它稱爲主線程,主線程又能夠啓動新的線程,threading 模塊中有一個函數 current_thread() ,能夠返回當前線程的實例。主線程實例的名字叫 MainThread,子線程的名字是在建立的時候指定,也就是 name 參數。

運行結果:

thread MainThread is running...
thread subthread-1 is running...
開始下載Python.pdf...
thread subthread-2 is running...
開始下載nazha.mkv...
nazha.mkv下載完成! 耗費了5秒
Python.pdf下載完成! 耗費了7秒
總共耗費了7.001秒
thread MainThread is running...
複製代碼
Lock

多線程和多進程最大的不一樣在於,多進程中,同一個變量,各自有一份拷貝存在於每一個進程中,互不影響,而多線程中,全部變量都由全部線程共享,因此,任何一個變量均可以被任何一個線程修改,所以,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了

下面是一個例子,演示了多線程同時操做一個變量,如何把內存給改亂了:

from threading import Thread
from time import time, sleep

# 假定這是你的銀行存款:
balance = 0


def change_it(n):
    # 先存後取,結果應該爲0:
    global balance
    balance = balance + n
    balance = balance - n


def run_thread(n):
    for i in range(100000):
        change_it(n)


def nolock_multi_thread():
    t1 = Thread(target=run_thread, args=(5,))
    t2 = Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(balance)


if __name__ == '__main__':
    nolock_multi_thread()
複製代碼

運行結果:

-8
複製代碼

代碼中定義了一個共享變量 balance,而後啓動兩個線程,先存後取,理論上結果應該是 0 。可是,因爲線程的調度是由操做系統決定的,當 t一、t2 交替執行時,只要循環次數足夠多,balance 的結果就不必定是0了。

緣由就是下面這條語句:

balance = balance + n
複製代碼

這條語句的執行分爲兩步的:

  • 先計算 balance + n,保存結果到一個臨時變量
  • 將臨時變量的值賦給 balance

也就是能夠當作:

x = balance+n
balance=x
複製代碼

正常運行以下所示:

初始值 balance = 0

t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1     # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1     # balance = 0

t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2     # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2     # balance = 0
    
結果 balance = 0
複製代碼

但實際上兩個線程是交替運行的,也就是:

初始值 balance = 0

t1: x1 = balance + 5  # x1 = 0 + 5 = 5

t2: x2 = balance + 8  # x2 = 0 + 8 = 8
t2: balance = x2      # balance = 8

t1: balance = x1      # balance = 5
t1: x1 = balance - 5  # x1 = 5 - 5 = 0
t1: balance = x1      # balance = 0

t2: x2 = balance - 8  # x2 = 0 - 8 = -8
t2: balance = x2   # balance = -8

結果 balance = -8
複製代碼

簡單說,就是由於對 balance 的修改須要多條語句,而執行這幾條語句的時候,線程可能中斷,致使多個線程把同個對象的內容該亂了。

要保證計算正確,須要給 change_it() 添加一個鎖,添加鎖後,其餘線程就必須等待當前線程執行完並釋放鎖,才能夠執行該函數。而且鎖是隻有一個,不管多少線程,同一時刻最多隻有一個線程持有該鎖。經過 threading 模塊的 Lock 實現。

所以代碼修改成:

from threading import Thread, Lock
from time import time, sleep

# 假定這是你的銀行存款:
balance = 0

lock = Lock()

def change_it(n):
    # 先存後取,結果應該爲0:
    global balance
    balance = balance + n
    balance = balance - n


def run_thread_lock(n):
    for i in range(100000):
        # 先要獲取鎖:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了必定要釋放鎖:
            lock.release()


def nolock_multi_thread():
    t1 = Thread(target=run_thread_lock, args=(5,))
    t2 = Thread(target=run_thread_lock, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(balance)


if __name__ == '__main__':
    nolock_multi_thread()
複製代碼

但遺憾的是 Python 並不能徹底發揮多線程的做用,這裏能夠經過寫一個死循環,而後經過任務管理器查看進程的 CPU 使用率。

正常來講,若是有兩個死循環線程,在多核CPU中,能夠監控到會佔用200%的CPU,也就是佔用兩個CPU核心。

要想把 N 核CPU的核心所有跑滿,就必須啓動 N 個死循環線程。

死循環代碼以下所示:

import threading, multiprocessing

def loop():
    x = 0
    while True:
        x = x ^ 1

for i in range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()
複製代碼

在 4 核CPU上能夠監控到 CPU 佔用率僅有102%,也就是僅使用了一核。

可是用其餘編程語言,好比C、C++或 Java來改寫相同的死循環,直接能夠把所有核心跑滿,4核就跑到400%,8核就跑到800%,爲何Python不行呢?

由於 Python 的線程雖然是真正的線程,但解釋器執行代碼時,有一個 GIL鎖:Global Interpreter Lock任何Python線程執行前,必須先得到GIL鎖,而後,每執行100條字節碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個 GIL 全局鎖實際上把全部線程的執行代碼都給上了鎖,因此,多線程在Python中只能交替執行,即便100個線程跑在100核CPU上,也只能用到1個核。

GIL是 Python 解釋器設計的歷史遺留問題,一般咱們用的解釋器是官方實現的 CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。

儘管多線程不能徹底利用多核,但對於程序的運行效率提高仍是很大的,若是想實現多核任務,能夠經過多進程實現多核任務。多個Python進程有各自獨立的GIL鎖,互不影響。

ThreadLocal

採用多線程的時候,一個線程採用本身的局部變量會比全局變量更好,緣由前面也介紹了,若是不加鎖,多個線程可能會亂改某個全局變量的數值,而局部變量是隻有每一個線程本身可見,不會影響其餘線程。

不過,局部變量的使用也有問題,就是函數調用時候,傳遞起來會比較麻煩,即以下所示:

def process_student(name):
    std = Student(name)
    # std是局部變量,可是每一個函數都要用它,所以必須傳進去:
    do_task_1(std)
    do_task_2(std)

def do_task_1(std):
    do_subtask_1(std)
    do_subtask_2(std)

def do_task_2(std):
    do_subtask_2(std)
    do_subtask_2(std)
複製代碼

局部變量須要一層層傳遞給每一個函數,比較麻煩,有沒有更好的辦法呢?

一個思路是用一個全局的 dict ,而後用每一個線程做爲 key ,代碼例子以下所示:

global_dict = {}

def std_thread(name):
    std = Student(name)
    # 把std放到全局變量global_dict中:
    global_dict[threading.current_thread()] = std
    do_task_1()
    do_task_2()

def do_task_1():
    # 不傳入std,而是根據當前線程查找:
    std = global_dict[threading.current_thread()]
    ...

def do_task_2():
    # 任何函數均可以查找出當前線程的std變量:
    std = global_dict[threading.current_thread()]
複製代碼

這種方式理論上是可行的,它能夠避免局部變量在每層函數中傳遞,只是獲取局部變量的代碼不夠優雅,在 threading 模塊中提供了 local 函數,能夠自動完成這件事情,代碼以下所示:

import threading
    
# 建立全局ThreadLocal對象:
local_school = threading.local()

def process_student():
    # 獲取當前線程關聯的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
    # 綁定ThreadLocal的student:
    local_school.student = name
    process_student()

t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
複製代碼

運行結果:

Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
複製代碼

在代碼中定義了一個全局變量 local_school ,它是一個 ThreadLocal 對象,每一個線程均可以對它讀寫 student 屬性,但又不會互相影響,也不須要管理鎖的問題,這是 ThreadLocal 內部會處理。

ThreadLocal 最經常使用的是爲每一個線程綁定一個數據庫鏈接,HTTP 請求,用戶身份信息等,這樣一個線程的全部調用到的處理函數均可以很是方便地訪問這些資源。

進程 vs 線程

咱們已經分別介紹了多進程和多線程的實現方式,那麼究竟應該選擇哪一種方法來實現併發編程呢,這二者有什麼優缺點呢?

一般多任務的實現,咱們都是設計 Master-WorkerMaster 負責分配任務,Worker 負責執行任務,所以多任務環境下,一般是一個 Master 和多個 Worker

若是用多進程實現 Master-Worker,主進程就是 Master,其餘進程就是 Worker

若是用多線程實現 Master-Worker,主線程就是 Master,其餘線程就是 Worker

對於多進程,最大的優勢就是穩定性高,由於一個子進程掛了,不會影響主進程和其餘子進程。固然主進程掛了,全部進程天然也就掛,但主進程只是負責分配任務,掛掉機率很是低。著名的 Apache 最先就是採用多進程模式。

缺點有:

  • 建立進程代價大,特別是在 windows 系統,開銷巨大,而 Unix/ Linux 系統由於能夠調用 fork() ,因此開銷還行;
  • 操做系統能夠同時運行的進程數量有限,會受到內存和 CPU 的限制

對於多線程,一般會快過多進程,但也不會快太多缺點就是穩定性很差,由於全部線程共享進程的內存,一個線程掛斷均可能直接形成整個進程崩潰。好比在Windows上,若是一個線程執行的代碼出了問題,你常常能夠看到這樣的提示:「該程序執行了非法操做,即將關閉」,其實每每是某個線程出了問題,可是操做系統會強制結束整個進程。

進程/線程切換

是否採用多任務模式,第一點須要注意的就是,一旦任務數量過多,效率確定上不去,這主要是切換進程或者線程是有代價的

操做系統在切換進程或者線程時的流程是這樣的:

  • 先保存當前執行的現場環境(CPU寄存器狀態、內存頁等)
  • 而後把新任務的執行環境準備好(恢復上次的寄存器狀態,切換內存頁等)
  • 開始執行任務

這個切換過程雖然很快,可是也須要耗費時間,若是任務數量有上千個,操做系統可能就忙着切換任務,而沒有時間執行任務,這種狀況最多見的就是硬盤狂響,點窗口無反應,系統處於假死狀態。

計算密集型vsI/O密集型

採用多任務的第二個考慮就是任務的類型,能夠將任務分爲計算密集型和 I/O 密集型

計算密集型任務的特色是要進行大量的計算,消耗CPU資源,好比對視頻進行編碼解碼或者格式轉換等等,這種任務全靠 CPU 的運算能力,雖然也能夠用多任務完成,可是任務越多,花在任務切換的時間就越多,CPU 執行任務的效率就越低。計算密集型任務因爲主要消耗CPU資源,這類任務用 Python這樣的腳本語言去執行效率一般很低,最能勝任這類任務的是C語言,咱們以前提到了 Python 中有嵌入 C/C++ 代碼的機制。不過,若是必須用 Python 來處理,那最佳的就是採用多進程,並且任務數量最好是等同於 CPU 的核心數。

除了計算密集型任務,其餘的涉及到網絡、存儲介質 I/O 的任務均可以視爲 I/O 密集型任務,這類任務的特色是 CPU 消耗不多,任務的大部分時間都在等待 I/O 操做完成(由於 I/O 的速度遠遠低於 CPU 和內存的速度)。對於 I/O 密集型任務,若是啓動多任務,就能夠減小 I/O 等待時間從而讓 CPU 高效率的運轉。通常會採用多線程來處理 I/O 密集型任務。

異步 I/O

現代操做系統對 I/O 操做的改進中最爲重要的就是支持異步 I/O。若是充分利用操做系統提供的異步 I/O 支持,就能夠用單進程單線程模型來執行多任務,這種全新的模型稱爲事件驅動模型。Nginx 就是支持異步 I/O的 Web 服務器,它在單核 CPU 上採用單進程模型就能夠高效地支持多任務。在多核 CPU 上,能夠運行多個進程(數量與CPU核心數相同),充分利用多核 CPU。用 Node.js 開發的服務器端程序也使用了這種工做模式,這也是當下實現多任務編程的一種趨勢。

在 Python 中,單線程+異步 I/O 的編程模型稱爲協程,有了協程的支持,就能夠基於事件驅動編寫高效的多任務程序協程最大的優點就是極高的執行效率,由於子程序切換不是線程切換,而是由程序自身控制,所以,沒有線程切換的開銷。協程的第二個優點就是不須要多線程的鎖機制,由於只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不用加鎖,只須要判斷狀態就行了,因此執行效率比多線程高不少。若是想要充分利用CPU的多核特性,最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可得到極高的性能。


參考

以上就是本次教程的全部內容,代碼已經上傳到:

github.com/ccc013/Pyth…

歡迎關注個人微信公衆號--算法猿的成長,或者掃描下方的二維碼,你們一塊兒交流,學習和進步!

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息