Python-16-スレッドプールとプロセスプールPython同時プログラミング

Python ThreadPoolExecutor スレッド プール
スレッド プールの基本原理は何ですか?
Python を使用してスレッド プールを迅速に実装する、非常に単純な
Python 同時プログラミングのトピック

1 同時プログラミング

1.1 同時プログラミングの概念

1. 同時プログラミングを導入する理由は何ですか?
シナリオ 1: Web クローラーが順次クロールするには 1 時間かかりましたが、同時ダウンロードでは 20 分に短縮されました。
シナリオ 2: APP アプリケーションの場合、最適化前にページを開くのに毎回 3 秒かかりますが、非同期同時実行性を使用して毎回 200 ミリ秒に増加します。
並行性の導入は、プログラムの実行速度を向上させることです。

次に、プログラムを高速化する方法は何でしょうか?
ここに画像の説明を挿入

3. Python の同時プログラミングのサポート
(1) マルチスレッド: CPU と IO が同時に実行できるという原理を使用したスレッド化。これにより、CPU は IO の完了を待たなくなります。
(2) マルチプロセス: マルチプロセッシングは、マルチコア CPU の機能を使用して、タスクを実際に並列実行します。
(3) 非同期 IO: asyncio は、単一スレッドでの CPU と IO の同時実行の原理を使用して、関数の非同期実行を実現します。
(4) Lock を使用してリソースをロックし、アクセスの競合を防ぎます。
(5) Queueを利用して異なるスレッド/プロセス間のデータ通信を実現し、プロデューサー・コンシューマーモードを実現します。
(6) スレッド プール プール/プロセス プール プールを使用して、スレッド/プロセスのタスクの送信、完了の待機、および結果の取得を簡素化します。
(7) サブプロセスを使用して外部プログラムのプロセスを起動し、入出力対話を行います。


Python の同時プログラミングには、マルチスレッド Thread、マルチプロセス Process、およびマルチコルーチン Coroutine の3 つの方法があります。

1.2 スレッド処理コルーチン

1. CPU 集中型コンピューティングと IO 集中型コンピューティングとは何ですか?
ここに画像の説明を挿入
2. マルチスレッド、マルチプロセス、マルチコルーチンの比較

ここに画像の説明を挿入
3. タスクに応じて対応するテクノロジーを選択するにはどうすればよいですか?
ここに画像の説明を挿入

1.3 グローバル インタプリタ ロック GIL

1. Python が遅い 2 つの理由
C/C++/JAVA と比較すると、Python は非常に遅く、特殊なシナリオによっては、Python は C++ の 100 ~ 200 倍遅くなります。速度が遅いため、大手企業 Ali/Tencent/Kuaishou の推奨エンジン、検索エンジン、ストレージ エンジン、および高パフォーマンス要件を必要とする低レベル モジュールなど、多くの企業のインフラストラクチャ コードは依然として C/C++ で開発されています。
ここに画像の説明を挿入
グローバル インタープリター ロック(英語: Global Interpreter Lock、略称 GIL)は、コンピューター プログラミング言語のインタープリターがスレッドを同期するために使用するメカニズムで、常に 1 つのスレッドのみの実行を許可します。マルチコア プロセッサ上でも、GIL を使用するインタープリタでは一度に 1 つのスレッドしか実行できません。
ここに画像の説明を挿入
2. なぜ GIL などというものがあるのでしょうか?
ここに画像の説明を挿入
3. GIL による制限を回避するにはどうすればよいですか?

ここに画像の説明を挿入

2 クローラー コード ブログ

import requests
from bs4 import BeautifulSoup

# 列表推导式获取url列表
urls = [
    f"https://www.cnblogs.com/sitehome/p/{page}"
    for page in range(1, 50 + 1)
]


def craw(url):
    # 爬取网页信息
    r = requests.get(url)
    return r.text


def parse(html):
    # 解析网页信息class="post-item-title"
    soup = BeautifulSoup(html, "html.parser")
    links = soup.find_all("a", class_="post-item-title")
    # 返回链接和文本信息
    return [(link["href"], link.get_text()) for link in links]


if __name__ == "__main__":
    for result in parse(craw(urls[2])):
        print(result)

3 クローラーを高速化するマルチスレッド化

3.1 マルチスレッドの作成方法

ここに画像の説明を挿入

3.2 シングルスレッドとマルチスレッドの比較

import blog
import threading
import time


def single_thread():
    print("single thread begin")
    # 循环遍历,逐步执行
    for url in blog.urls:
        blog.craw(url)
    print("single thread end")


def multi_thread():
    print("multi thread begin")
    threads = []
    # 每个链接创建一个线程
    for url in blog.urls:
        threads.append(threading.Thread(target=blog.craw, args=(url,)))
        
    # 逐个启动线程
    for thread in threads:
        thread.start()
        
    # 等待运行结束(阻塞主线程)
    for thread in threads:
        thread.join()

    print("multi thread end")


if __name__ == "__main__":
    start = time.time()
    single_thread()
    end = time.time()
    print("single thread cost:", end - start, "seconds")

    start = time.time()
    multi_thread()
    end = time.time()
    print("multi thread cost:", end - start, "seconds")

ここに画像の説明を挿入

4 プロデューサー コンシューマー モード マルチスレッド クローラー

4.1 プロデューサー コンシューマー アーキテクチャ

1. マルチコンポーネント パイプライン テクノロジ アーキテクチャ
複雑なことは通常、一度にすべて実行されるのではなく、多くの中間ステップを経て段階的に完了します。
ここに画像の説明を挿入
2. プロデューサー コンシューマー クローラーのアーキテクチャ
ここに画像の説明を挿入
3. マルチスレッド データ通信用の queue.Queue queue.Queue は、
複数のスレッド間のスレッドセーフなデータ通信に使用できます。
ここに画像の説明を挿入

4.2 プロデューサー・コンシューマー・コード

import queue
import blog
import time
import random
import threading


def do_craw(url_queue: queue.Queue, html_queue: queue.Queue):
    while True:
        url = url_queue.get()  # 从队列中获取url
        html = blog.craw(url)
        html_queue.put(html)  # 爬取的网页信息写入队列
        print(threading.current_thread().name,
              threading.current_thread().ident,
              f"craw {url}",
              "url_queue.size=", url_queue.qsize())
        # 随机休眠1或2秒
        time.sleep(random.randint(1, 2))


def do_parse(html_queue: queue.Queue, fout):
    while True:
        html = html_queue.get()
        results = blog.parse(html)
        for result in results:
            fout.write(str(result) + "\n")
        print(threading.current_thread().name,
              threading.current_thread().ident,
              f"results.size", len(results),
              "html_queue.size=", html_queue.qsize())
        time.sleep(random.randint(1, 2))


if __name__ == "__main__":
    url_queue = queue.Queue()  # 待爬取的url队列
    html_queue = queue.Queue()  # 爬取的网页信息队列
    for url in blog.urls:
        url_queue.put(url)
    # 启动3个线程,爬取网页信息
    for idx in range(3):
        t = threading.Thread(target=do_craw, args=(url_queue, html_queue),
                             name=f"craw{idx}")
        t.start()
    # 启动2个线程,写入文件
    fout = open("02.data.txt", "w")
    for idx in range(2):
        t = threading.Thread(target=do_parse, args=(html_queue, fout),
                             name=f"parse{idx}")
        t.start()
    print("jiesu")

ここに画像の説明を挿入

5 スレッドの安全性の問題

5.1 スレッドセーフティの概念

スレッド セーフとは、関数または関数ライブラリがマルチスレッド環境で呼び出されたときに、複数のスレッド間で共有変数を正しく処理できるため、プログラム関数が正しく完了できることを意味します。
スレッドの実行はいつでも切り替わるため、予期しない結果が発生し、スレッドは安全ではありません。
ここに画像の説明を挿入

import threading
import time

lock = threading.Lock()


class Account:
    def __init__(self, balance):
        self.balance = balance


def draw(account, amount):
    # with lock:
    if account.balance >= amount:
        time.sleep(0.1)
        print(threading.current_thread().name, "取钱成功")
        account.balance -= amount
        print(threading.current_thread().name, "余额", account.balance)
    else:
        print(threading.current_thread().name, "取钱失败,余额不足")


if __name__ == "__main__":
    account = Account(1000)
    ta = threading.Thread(name="ta", target=draw, args=(account, 800))
    tb = threading.Thread(name="tb", target=draw, args=(account, 800))

    ta.start()
    tb.start()

ここに画像の説明を挿入

5.2 スレッドの安全性の問題の解決

ここに画像の説明を挿入

import threading
import time

lock = threading.Lock()


class Account:
    def __init__(self, balance):
        self.balance = balance


def draw(account, amount):
    with lock:
        if account.balance >= amount:
            time.sleep(0.1)
            print(threading.current_thread().name, "取钱成功")
            account.balance -= amount
            print(threading.current_thread().name, "余额", account.balance)
        else:
            print(threading.current_thread().name, "取钱失败,余额不足")


if __name__ == "__main__":
    account = Account(1000)
    ta = threading.Thread(name="ta", target=draw, args=(account, 800))
    tb = threading.Thread(name="tb", target=draw, args=(account, 800))

    ta.start()
    tb.start()

ここに画像の説明を挿入

6 つの便利なスレッド プール

(1) 資源の消費量を削減します。作成されたスレッドを再利用することで、スレッドの作成と破棄の消費を削減します。
(2) 応答速度を向上させます。タスクが到着すると、スレッドの作成を待たずにすぐにタスクを実行できます。
(3) スレッドの管理性を向上します。スレッドは希少なリソースです。無制限に作成すると、システム リソースを消費するだけでなく、システムの安定性も低下します。スレッド プールを使用すると、一元的な割り当て、チューニング、監視が可能になります。

ここに画像の説明を挿入

Python にはすでにスレッド モジュールがありますが、なぜスレッド プールが必要なのでしょうか。また、スレッド プールとは何ですか? クローラーを例にとると、同時にクロールするスレッドの数を制御する必要があります。たとえば、20 個のスレッドが作成され、同時に実行できるスレッドは 3 つだけですが、20 個のスレッドすべてを実行する必要があります。作成と破棄が行われ、スレッドの作成にはシステム リソースが消費される必要があります。より良い解決策はありますか?

実際には、必要なスレッドは 3 つだけです。各スレッドにはタスクが割り当てられ、残りのタスクはキューに入れられます。スレッドがタスクを完了すると、キューに入れられたタスクをこのスレッドに割り当てて実行を継続できます。

しかし、完璧なスレッドプールを自分で書くのは難しく、複雑な状況ではスレッドの同期も考慮する必要があり、デッドロックが発生しやすくなります。Python3.2 以降、標準ライブラリは concurrent.futures モジュールを提供します。このモジュールは、スレッドとマルチプロセッシングのさらなる抽象化を実現するために、ThreadPoolExecutor と ProcessPoolExecutor という 2 つのクラスを提供します (ここでは主にスレッド プールに焦点を当てます)。

6.1 スレッドプールの原理

ここに画像の説明を挿入
1. パフォーマンスの向上: スレッドの作成と終了のオーバーヘッドが軽減されるため、スレッド リソースが再利用されます。 2. 適用
可能なシナリオ: 大量の突然のリクエストの処理や、タスクを完了するために多数のスレッドが必要な場合に適していますが、実際のタスクは処理時間が比較的短い;
3. 防御機能: 作成されるスレッドが多すぎることによる過剰なシステム負荷などの問題を効果的に回避できます; 4. コードの利点:
スレッド プールを使用する構文は、新しいスレッドを作成するよりも簡潔ですスレッドを実行します。

6.2 ThreadPoolExecutorの使用法

ここに画像の説明を挿入

import concurrent.futures
import blog

# craw爬取
with concurrent.futures.ThreadPoolExecutor() as pool:
    htmls = pool.map(blog.craw, blog.urls)
    htmls = list(zip(blog.urls, htmls))
    for url, html in htmls:
        print(url, len(html))

print("craw over")

# parse解析
with concurrent.futures.ThreadPoolExecutor() as pool:
    futures = {
    
    }
    for url, html in htmls:
        future = pool.submit(blog.parse, html)
        futures[future] = url
    # 方式一:按顺序返回
    for future, url in futures.items():
        print(url, future.result())
    # 方式二:先完成的先返回
    # for future in concurrent.futures.as_completed(futures):
    #     url = futures[future]
    #     print(url, future.result())

6.3 Web サービスでのスレッド プール アクセラレーションの使用

1. Web サービスのアーキテクチャと特性
ここに画像の説明を挿入
ThreadPoolExecutor を使用する利点:
1. ディスク ファイル、データベース、およびリモート API の IO 呼び出しを同時に実行すると便利です;
2. スレッド プール内のスレッドの数は、無限に作成されない (システムがハングする原因となる) ため、防御機能があります。
2. コードは Flask を使用して Web サービスを実装し、
5 秒間の高速化後に結果を達成します。

import flask
import json
import time
from concurrent.futures import ThreadPoolExecutor

app = flask.Flask(__name__)
pool = ThreadPoolExecutor()


def read_file():
    time.sleep(5)
    return "file result"


def read_db():
    time.sleep(4)
    return "db result"


def read_api():
    time.sleep(3)
    return "api result"


@app.route("/")
def index():
    result_file = pool.submit(read_file)
    result_db = pool.submit(read_db)
    result_api = pool.submit(read_api)

    return json.dumps({
    
    
        "result_file": result_file.result(),
        "result_db": result_db.result(),
        "result_api": result_api.result(),
    })


if __name__ == "__main__":
    app.run()

7 つの便利なプロセス プール

7.1 マルチプロセスとマルチスレッド

1. マルチスレッド スレッドでは、なぜマルチプロセス マルチプロセッシングを使用するのですか?
ここに画像の説明を挿入
2. マルチプロセス マルチプロセッシングとマルチスレッド スレッド
ここに画像の説明を挿入

7.2 CPU 負荷の高い計算速度の比較

素数は素数とも呼ばれます。1より大きい自然数は、1とそれ自体を除いて、素数と呼ばれる他の自然数で割り切れず、それ以外の場合は合成数と呼ばれます(1は素数でも合成数でもないと規定されています)。
CPU負荷の高い計算:「大きな数が素数かどうかを判断する」計算を100回。

import math
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time

PRIMES = [112272535095293] * 100


def is_prime(n):
    if n == 1:
        return False
    for i in range(2, int(math.sqrt(n))+1):
        if n % i == 0:
            return False
    return True


def single_thread():
    for number in PRIMES:
        is_prime(number)


def multi_thread():
    with ThreadPoolExecutor() as pool:
        pool.map(is_prime, PRIMES)


def multi_process():
    with ProcessPoolExecutor() as pool:
        pool.map(is_prime, PRIMES)


if __name__ == "__main__":
    start = time.time()
    single_thread()
    end = time.time()
    print("single thread, cost:", end - start, "seconds")

    start = time.time()
    multi_thread()
    end = time.time()
    print("multi thread, cost:", end - start, "seconds")

    start = time.time()
    multi_process()
    end = time.time()
    print("multi process, cost:", end - start, "seconds")

ここに画像の説明を挿入
GIL の存在により、マルチスレッドの計算はシングルスレッドの計算より遅くなりますが、マルチ処理では実行を大幅に高速化できます。

7.3 Web サービスでのプロセス プール アクセラレーションの使用

http://127.0.0.1:5000/is_prime/1001245678353,3257385365375634564,3432434345657677

import flask
from concurrent.futures import ProcessPoolExecutor
import math
import json


app = flask.Flask(__name__)


def is_prime(n):
    if n == 1:
        return False
    for i in range(2, int(math.sqrt(n))+1):
        if n % i == 0:
            return False
    return True


@app.route("/is_prime/<numbers>")
def api_is_prime(numbers):
    number_list = [int(x) for x in numbers.split(",")]
    results = process_pool.map(is_prime, number_list)
    return json.dumps(dict(zip(number_list, results)))


if __name__ == "__main__":
    process_pool = ProcessPoolExecutor()
    app.run()

ここに画像の説明を挿入

8 非同期 IO は同時クローラーを実装します

8.1 コルーチンの原理

ここに画像の説明を挿入

ここに画像の説明を挿入

import asyncio
import aiohttp
import blog


# async语法进行声明为异步协程方法
# await语法进行声明为异步协程可等待对象
async def async_craw(url):
    print("craw url: ", url)
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            result = await resp.text()
            print(f"craw url: {url}, {len(result)}")

# 获取事件循环
loop = asyncio.get_event_loop()

# 创建task列表
tasks = [
    loop.create_task(async_craw(url))
    for url in blog.urls]

import time

start = time.time()
# 执行爬虫事件列表
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print("use time seconds: ", end - start)

8.2 セマフォを使用してクローラの同時実行性を制御する

ここに画像の説明を挿入

import asyncio
import aiohttp
import blog

semaphore = asyncio.Semaphore(10)


async def async_craw(url):
    async with semaphore:
        print("craw url: ", url)
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                result = await resp.text()
                await asyncio.sleep(2)
                print(f"craw url: {url}, {len(result)}")


loop = asyncio.get_event_loop()

tasks = [
    loop.create_task(async_craw(url))
    for url in blog.urls]

import time

start = time.time()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print("use time seconds: ", end - start)

付録 ThreadPoolExecutor スレッド プール

共通機能

関数が実行のためにスレッド プールに送信されると、Future オブジェクトが自動的に作成されて返されます。この Future オブジェクトには、関数の実行ステータス (現時点で一時停止しているか、実行中か、完了しているかなど) が含まれます。また、関数の実行後は、future.set_result を呼び出して独自の戻り値を設定します。
(1) スレッド プールを作成するには、max_workers パラメータを指定して、最大で作成されるスレッドの数を指定できます。指定しない場合、送信された関数ごとにスレッドが作成されます。

スレッド プールを開始するときは、必ず容量を設定する必要があります。設定しないと、数千の関数を処理するために数千のスレッドを開く必要があります。

(2) 関数は submit を通じてスレッド プールに送信でき、送信されるとすぐに実行されます。新しいスレッドが開かれるため、メインスレッドは実行を継続します。submitのパラメータは関数名に従って、対応するパラメータをsubmitすることができます。

(3) future はコンテナに相当し、内部関数の実行ステータスが含まれます。

(4)# 関数が実行されると、戻り値は将来に設定されます。つまり、future.set_result が実行されると、関数の実行が完了したことを意味し、外部から result を呼び出すことができます。戻り値を取得します。

from concurrent.futures import ThreadPoolExecutor
import time


def task(name, n):
    time.sleep(n)
    return f"{name} 睡了 {n} 秒"


executor = ThreadPoolExecutor()
future = executor.submit(task, "屏幕前的你", 3)

print(future)  # <Future at 0x7fbf701726d0 state=running
print(future.running())  # 函数是否正在运行中True
print(future.done())  # 函数是否执行完毕False

time.sleep(3)  # 主程序也sleep 3秒,显然此时函数已经执行完毕了

print(future)  # <Future at 0x7fbf701726d0 state=finished returned str>返回值类型是str
print(future.running())  # False
print(future.done())  # True

print(future.result())

コールバックを追加

注: future.result()、このステップはブロックされます。future.result() は関数の戻り値を取得します。したがって、最初に関数が実行されるのを待つことしかできず、set_result を通じて戻り値を未来に設定した後、外部は future.result() を呼び出して値を取得できます。

future には、_result と _state という 2 つの保護されたプロパティがあります。明らかに、_result は関数の戻り値を保存するために使用され、future.result() は基本的に _result 属性の値を返します。_state 属性は、関数の実行状態を示すために使用されます。初期状態は PENDING、実行中は RUNING、実行が完了すると FINISHED に設定されます。

future.result()を呼び出すと_stateの属性が判定され、まだ実行中の場合は永遠に待ちます。_state が FINISHED の場合、_result 属性の値が返されます。

executor = ThreadPoolExecutor()
future = executor.submit(task, "屏幕前的你", 3)
start = time.perf_counter()
future.result()
end = time.perf_counter()
print(end - start)  # 3.009

関数がいつ実行されるかわからないからです。したがって、最良の方法はコールバックをバインドすることです。関数が実行されると、コールバックが自動的にトリガーされます。
submit メソッドが呼び出された後、スレッド プールにサブミットされた関数はすでに実行を開始していることに注意してください。関数が実行されたかどうかに関係なく、コールバックを対応するフューチャーにバインドできます。

関数が完了する前にコールバックが追加された場合、コールバックは関数の完了後にトリガーされます。
関数の完了後にコールバックが追加された場合、関数は完了しているため、現時点で future に値があるか、set_result が設定されていることを意味し、コールバックはすぐにトリガーされます。

from concurrent.futures import ThreadPoolExecutor
import time


def task(name, n):
    time.sleep(n)
    return f"{name} 睡了 {n} 秒"


def callback(f):
    print("我是回调",f.result())

executor = ThreadPoolExecutor()
future = executor.submit(task, "自我休眠", 3)
# time.sleep(5)
# 绑定回调,3秒之后自动调用
future.add_done_callback(callback)

関数を実行するために複数のスレッドを開始する必要がある場合は、スレッド プールを使用することもできます。関数が呼び出されるたびに、スレッドがプールから取り出され、関数が実行された後、他の関数が実行できるようにそのスレッドがプールに戻されます。プールが空であるか、新しいアイドル スレッドを作成できない場合、次の関数は待機状態になることしかできません。

付録 ProcessPoolExecutor プロセス プール

concurrent.futures はスレッド プールの実装だけでなく、プロセス プールの実装にも使用でき、API は同じですが、作業中にプロセス プールが作成されることはほとんどありません。

おすすめ

転載: blog.csdn.net/qq_20466211/article/details/130687063