Python中並行與並發和AsyncIO之間的區別

本教程著眼於如何使用多處理、線程和 AsyncIO 加速 CPU 密集型和 IO 密集型操作。

並發與並行

並發和並行是相似的術語,但它們不是一回事。

並發是在 CPU 上同時運行多個任務的能力。任務可以在重疊的時間段內開始、運行和完成。在單個 CPU 的情況下,多個任務在上下文切換的幫助下運行,其中存儲了進程的狀態,以便以後可以調用和執行。

與此同時,並行性是跨多個 CPU 內核同時運行多個任務的能力。

儘管它們可以提高應用程序的速度,但不應在任何地方使用並發和並行性。用例取決於任務是 CPU 密集型還是 IO 密集型。

受 CPU 限制的任務是 CPU 綁定的。例如,數學計算受 CPU 限制,因為計算能力隨著計算機處理器數量的增加而增加。並行性適用於 CPU 密集型任務。理論上,如果將一個任務分成n個子任務,這些n個任務中的每一個都可以並行運行,從而有效地將時間減少到原來非並行任務的1/n。IO 綁定任務首選並發,因為您可以在獲取 IO 資源時執行其他操作。

CPU 密集型任務的最佳示例是數據科學。數據科學家處理大量數據。對於數據預處理,他們可以將數據分成多個批次並並行運行,從而有效地減少總處理時間。增加內核數量可以加快處理速度。

Web 抓取受 IO 限制。因為任務對 CPU 的影響很小,因為大部分時間都花在讀寫網絡上。其他常見的 IO-bound 任務包括數據庫調用和讀寫文件到磁盤。Web 應用程序,如 Django 和 Flask,是 IO-bound 應用程序。

如果您有興趣詳細了解 Python 中的線程、多處理和異步之間的區別,請查看使用並發、並行和異步加速 Python 的文章。

設想

有了這個,讓我們看看如何加速以下任務:

# tasks.py

import os
from multiprocessing import current_process
from threading import current_thread

import requests


def make_request(num):
    # io-bound

    pid = os.getpid()
    thread_name = current_thread().name
    process_name = current_process().name
    print(f"{pid} - {process_name} - {thread_name}")

    requests.get("https://httpbin.org/ip")


async def make_request_async(num, client):
    # io-bound

    pid = os.getpid()
    thread_name = current_thread().name
    process_name = current_process().name
    print(f"{pid} - {process_name} - {thread_name}")

    await client.get("https://httpbin.org/ip")


def get_prime_numbers(num):
    # cpu-bound

    pid = os.getpid()
    thread_name = current_thread().name
    process_name = current_process().name
    print(f"{pid} - {process_name} - {thread_name}")

    numbers = []

    prime = [True for i in range(num + 1)]
    p = 2

    while p * p <= num:
        if prime[p]:
            for i in range(p * 2, num + 1, p):
                prime[i] = False
        p += 1

    prime[0] = False
    prime[1] = False

    for p in range(num + 1):
        if prime[p]:
            numbers.append(p)

    return numbers

本教程中的所有代碼示例都可以在parallel-concurrent-examples-python存儲庫中找到。

筆記:

我們將使用標準庫中的以下庫來加速上述任務:

圖書館類/方法加工類型
穿線同時
並發期貨線程池執行器同時
異步收集並發(通過協程)
多處理水池平行
並發期貨ProcessPoolExecutor平行

IO綁定操作

同樣,IO 密集型任務在 IO 上花費的時間比在 CPU 上的時間要多。

由於網頁抓取受 IO 限制,我們應該使用線程來加快處理速度,因為 HTML (IO) 的檢索比解析它 (CPU) 慢。

場景:如何加速基於 Python 的網頁抓取和爬取腳本?

同步示例

讓我們從一個基准開始。

# io-bound_sync.py

import time

from tasks import make_request


def main():
    for num in range(1, 101):
        make_request(num)


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,我們使用該make_request函數發出了 100 個 HTTP 請求。由於請求是同步發生的,因此每個任務都是按順序執行的。

Elapsed run time: 15.710984757 seconds.

因此,每個請求大約需要 0.16 秒。

線程示例

# io-bound_concurrent_1.py

import threading
import time

from tasks import make_request


def main():
    tasks = []

    for num in range(1, 101):
        tasks.append(threading.Thread(target=make_request, args=(num,)))
        tasks[-1].start()

    for task in tasks:
        task.join()


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,同一個make_request函數被調用了 100 次。這次該threading庫用於為每個請求創建一個線程。

Elapsed run time: 1.020112515 seconds.

總時間從~16s 減少到~1s。

由於我們為每個請求使用單獨的線程,您可能想知道為什麼整個事情沒有花費大約 0.16 秒來完成。這個額外的時間是管理線程的開銷。Python 中的全局解釋器鎖(GIL) 確保一次只有一個線程使用 Python 字節碼。

concurrent.futures 示例

# io-bound_concurrent_2.py

import time
from concurrent.futures import ThreadPoolExecutor, wait

from tasks import make_request


def main():
    futures = []

    with ThreadPoolExecutor() as executor:
        for num in range(1, 101):
            futures.append(executor.submit(make_request, num))

    wait(futures)


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

這裡我們用來concurrent.futures.ThreadPoolExecutor實現多線程。在創建了所有期貨/承諾之後,我們過去常常wait等待它們全部完成。

Elapsed run time: 1.340592231 seconds

concurrent.futures.ThreadPoolExecutor實際上是對multithreading庫的抽象,這使得它更易於使用。在前面的示例中,我們將每個請求分配給一個線程,總共使用了 100 個線程。但ThreadPoolExecutor默認工作線程數為min(32, os.cpu_count() + 4). ThreadPoolExecutor 的存在是為了簡化實現多線程的過程。如果您想對多線程進行更多控制,請改用該multithreading庫。

AsyncIO 示例

# io-bound_concurrent_3.py

import asyncio
import time

import httpx

from tasks import make_request_async


async def main():
    async with httpx.AsyncClient() as client:
        return await asyncio.gather(
            *[make_request_async(num, client) for num in range(1, 101)]
        )


if __name__ == "__main__":
    start_time = time.perf_counter()

    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    end_time = time.perf_counter()
    elapsed_time = end_time - start_time
    print(f"Elapsed run time: {elapsed_time} seconds")

httpxrequests因為不支持異步操作,所以在這裡使用。

在這裡,我們用來asyncio實現並發。

Elapsed run time: 0.553961068 seconds

asyncio比其他方法更快,因為threading使用了 OS(操作系統)線程。因此線程由操作系統管理,其中線程切換由操作系統搶占。asyncio使用由 Python 解釋器定義的協程。使用協程,程序可以決定何時以最佳方式切換任務。這由even_loopin asyncio 處理。

CPU 密集型操作

場景:如何加速一個簡單的數據處理腳本?

同步示例

再次,讓我們從基准開始。

# cpu-bound_sync.py

import time

from tasks import get_prime_numbers


def main():
    for num in range(1000, 16000):
        get_prime_numbers(num)


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,我們對從 1000 到 16000 的數字執行了get_prime_numbers函數。

Elapsed run time: 17.863046316 seconds.

多處理示例

# cpu-bound_parallel_1.py

import time
from multiprocessing import Pool, cpu_count

from tasks import get_prime_numbers


def main():
    with Pool(cpu_count() - 1) as p:
        p.starmap(get_prime_numbers, zip(range(1000, 16000)))
        p.close()
        p.join()


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,我們用來multiprocessing計算素數。

Elapsed run time: 2.9848740599999997 seconds.

concurrent.futures 示例

# cpu-bound_parallel_2.py

import time
from concurrent.futures import ProcessPoolExecutor, wait
from multiprocessing import cpu_count

from tasks import get_prime_numbers


def main():
    futures = []

    with ProcessPoolExecutor(cpu_count() - 1) as executor:
        for num in range(1000, 16000):
            futures.append(executor.submit(get_prime_numbers, num))

    wait(futures)


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,我們使用concurrent.futures.ProcessPoolExecutor. 將作業添加到期貨後,wait(futures)等待它們完成。

Elapsed run time: 4.452427557 seconds.

concurrent.futures.ProcessPoolExecutor是一個包裝器multiprocessing.Pool。它具有與ThreadPoolExecutor. 如果您想更好地控制多處理,請使用multiprocessing.Pool. concurrent.futures提供了對多處理和線程的抽象,使其易於在兩者之間切換。

結論

值得注意的是,使用多處理來執行make_request函數將比線程風格慢得多,因為進程需要等待 IO。不過,多處理方法將比同步方法更快。

同樣,與並行性相比,對 CPU 密集型任務使用並發性也不值得。

話雖如此,使用並發性或併行性來執行腳本會增加複雜性。您的代碼通常更難閱讀、測試和調試,因此只有在絕對需要長時間運行的腳本時才使用它們。

concurrent.futures是我通常開始的地方-

  1. 在並發和並行之間來回切換很容易
  2. 依賴庫不需要支持 asyncio ( requestsvs httpx)
  3. 它比其他方法更乾淨,更容易閱讀

從GitHub 上的parallel-concurrent-examples-python存儲庫中獲取代碼。

來源:  https ://testdriven.io

#python #asyncio 

What is GEEK

Buddha Community

Python中並行與並發和AsyncIO之間的區別

Python中並行與並發和AsyncIO之間的區別

本教程著眼於如何使用多處理、線程和 AsyncIO 加速 CPU 密集型和 IO 密集型操作。

並發與並行

並發和並行是相似的術語,但它們不是一回事。

並發是在 CPU 上同時運行多個任務的能力。任務可以在重疊的時間段內開始、運行和完成。在單個 CPU 的情況下,多個任務在上下文切換的幫助下運行,其中存儲了進程的狀態,以便以後可以調用和執行。

與此同時,並行性是跨多個 CPU 內核同時運行多個任務的能力。

儘管它們可以提高應用程序的速度,但不應在任何地方使用並發和並行性。用例取決於任務是 CPU 密集型還是 IO 密集型。

受 CPU 限制的任務是 CPU 綁定的。例如,數學計算受 CPU 限制,因為計算能力隨著計算機處理器數量的增加而增加。並行性適用於 CPU 密集型任務。理論上,如果將一個任務分成n個子任務,這些n個任務中的每一個都可以並行運行,從而有效地將時間減少到原來非並行任務的1/n。IO 綁定任務首選並發,因為您可以在獲取 IO 資源時執行其他操作。

CPU 密集型任務的最佳示例是數據科學。數據科學家處理大量數據。對於數據預處理,他們可以將數據分成多個批次並並行運行,從而有效地減少總處理時間。增加內核數量可以加快處理速度。

Web 抓取受 IO 限制。因為任務對 CPU 的影響很小,因為大部分時間都花在讀寫網絡上。其他常見的 IO-bound 任務包括數據庫調用和讀寫文件到磁盤。Web 應用程序,如 Django 和 Flask,是 IO-bound 應用程序。

如果您有興趣詳細了解 Python 中的線程、多處理和異步之間的區別,請查看使用並發、並行和異步加速 Python 的文章。

設想

有了這個,讓我們看看如何加速以下任務:

# tasks.py

import os
from multiprocessing import current_process
from threading import current_thread

import requests


def make_request(num):
    # io-bound

    pid = os.getpid()
    thread_name = current_thread().name
    process_name = current_process().name
    print(f"{pid} - {process_name} - {thread_name}")

    requests.get("https://httpbin.org/ip")


async def make_request_async(num, client):
    # io-bound

    pid = os.getpid()
    thread_name = current_thread().name
    process_name = current_process().name
    print(f"{pid} - {process_name} - {thread_name}")

    await client.get("https://httpbin.org/ip")


def get_prime_numbers(num):
    # cpu-bound

    pid = os.getpid()
    thread_name = current_thread().name
    process_name = current_process().name
    print(f"{pid} - {process_name} - {thread_name}")

    numbers = []

    prime = [True for i in range(num + 1)]
    p = 2

    while p * p <= num:
        if prime[p]:
            for i in range(p * 2, num + 1, p):
                prime[i] = False
        p += 1

    prime[0] = False
    prime[1] = False

    for p in range(num + 1):
        if prime[p]:
            numbers.append(p)

    return numbers

本教程中的所有代碼示例都可以在parallel-concurrent-examples-python存儲庫中找到。

筆記:

我們將使用標準庫中的以下庫來加速上述任務:

圖書館類/方法加工類型
穿線同時
並發期貨線程池執行器同時
異步收集並發(通過協程)
多處理水池平行
並發期貨ProcessPoolExecutor平行

IO綁定操作

同樣,IO 密集型任務在 IO 上花費的時間比在 CPU 上的時間要多。

由於網頁抓取受 IO 限制,我們應該使用線程來加快處理速度,因為 HTML (IO) 的檢索比解析它 (CPU) 慢。

場景:如何加速基於 Python 的網頁抓取和爬取腳本?

同步示例

讓我們從一個基准開始。

# io-bound_sync.py

import time

from tasks import make_request


def main():
    for num in range(1, 101):
        make_request(num)


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,我們使用該make_request函數發出了 100 個 HTTP 請求。由於請求是同步發生的,因此每個任務都是按順序執行的。

Elapsed run time: 15.710984757 seconds.

因此,每個請求大約需要 0.16 秒。

線程示例

# io-bound_concurrent_1.py

import threading
import time

from tasks import make_request


def main():
    tasks = []

    for num in range(1, 101):
        tasks.append(threading.Thread(target=make_request, args=(num,)))
        tasks[-1].start()

    for task in tasks:
        task.join()


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,同一個make_request函數被調用了 100 次。這次該threading庫用於為每個請求創建一個線程。

Elapsed run time: 1.020112515 seconds.

總時間從~16s 減少到~1s。

由於我們為每個請求使用單獨的線程,您可能想知道為什麼整個事情沒有花費大約 0.16 秒來完成。這個額外的時間是管理線程的開銷。Python 中的全局解釋器鎖(GIL) 確保一次只有一個線程使用 Python 字節碼。

concurrent.futures 示例

# io-bound_concurrent_2.py

import time
from concurrent.futures import ThreadPoolExecutor, wait

from tasks import make_request


def main():
    futures = []

    with ThreadPoolExecutor() as executor:
        for num in range(1, 101):
            futures.append(executor.submit(make_request, num))

    wait(futures)


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

這裡我們用來concurrent.futures.ThreadPoolExecutor實現多線程。在創建了所有期貨/承諾之後,我們過去常常wait等待它們全部完成。

Elapsed run time: 1.340592231 seconds

concurrent.futures.ThreadPoolExecutor實際上是對multithreading庫的抽象,這使得它更易於使用。在前面的示例中,我們將每個請求分配給一個線程,總共使用了 100 個線程。但ThreadPoolExecutor默認工作線程數為min(32, os.cpu_count() + 4). ThreadPoolExecutor 的存在是為了簡化實現多線程的過程。如果您想對多線程進行更多控制,請改用該multithreading庫。

AsyncIO 示例

# io-bound_concurrent_3.py

import asyncio
import time

import httpx

from tasks import make_request_async


async def main():
    async with httpx.AsyncClient() as client:
        return await asyncio.gather(
            *[make_request_async(num, client) for num in range(1, 101)]
        )


if __name__ == "__main__":
    start_time = time.perf_counter()

    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    end_time = time.perf_counter()
    elapsed_time = end_time - start_time
    print(f"Elapsed run time: {elapsed_time} seconds")

httpxrequests因為不支持異步操作,所以在這裡使用。

在這裡,我們用來asyncio實現並發。

Elapsed run time: 0.553961068 seconds

asyncio比其他方法更快,因為threading使用了 OS(操作系統)線程。因此線程由操作系統管理,其中線程切換由操作系統搶占。asyncio使用由 Python 解釋器定義的協程。使用協程,程序可以決定何時以最佳方式切換任務。這由even_loopin asyncio 處理。

CPU 密集型操作

場景:如何加速一個簡單的數據處理腳本?

同步示例

再次,讓我們從基准開始。

# cpu-bound_sync.py

import time

from tasks import get_prime_numbers


def main():
    for num in range(1000, 16000):
        get_prime_numbers(num)


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,我們對從 1000 到 16000 的數字執行了get_prime_numbers函數。

Elapsed run time: 17.863046316 seconds.

多處理示例

# cpu-bound_parallel_1.py

import time
from multiprocessing import Pool, cpu_count

from tasks import get_prime_numbers


def main():
    with Pool(cpu_count() - 1) as p:
        p.starmap(get_prime_numbers, zip(range(1000, 16000)))
        p.close()
        p.join()


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,我們用來multiprocessing計算素數。

Elapsed run time: 2.9848740599999997 seconds.

concurrent.futures 示例

# cpu-bound_parallel_2.py

import time
from concurrent.futures import ProcessPoolExecutor, wait
from multiprocessing import cpu_count

from tasks import get_prime_numbers


def main():
    futures = []

    with ProcessPoolExecutor(cpu_count() - 1) as executor:
        for num in range(1000, 16000):
            futures.append(executor.submit(get_prime_numbers, num))

    wait(futures)


if __name__ == "__main__":
    start_time = time.perf_counter()

    main()

    end_time = time.perf_counter()
    print(f"Elapsed run time: {end_time - start_time} seconds.")

在這裡,我們使用concurrent.futures.ProcessPoolExecutor. 將作業添加到期貨後,wait(futures)等待它們完成。

Elapsed run time: 4.452427557 seconds.

concurrent.futures.ProcessPoolExecutor是一個包裝器multiprocessing.Pool。它具有與ThreadPoolExecutor. 如果您想更好地控制多處理,請使用multiprocessing.Pool. concurrent.futures提供了對多處理和線程的抽象,使其易於在兩者之間切換。

結論

值得注意的是,使用多處理來執行make_request函數將比線程風格慢得多,因為進程需要等待 IO。不過,多處理方法將比同步方法更快。

同樣,與並行性相比,對 CPU 密集型任務使用並發性也不值得。

話雖如此,使用並發性或併行性來執行腳本會增加複雜性。您的代碼通常更難閱讀、測試和調試,因此只有在絕對需要長時間運行的腳本時才使用它們。

concurrent.futures是我通常開始的地方-

  1. 在並發和並行之間來回切換很容易
  2. 依賴庫不需要支持 asyncio ( requestsvs httpx)
  3. 它比其他方法更乾淨,更容易閱讀

從GitHub 上的parallel-concurrent-examples-python存儲庫中獲取代碼。

來源:  https ://testdriven.io

#python #asyncio