【PYTHON并发学习】多线程Threading+多进程Multiprocessing+多协程Asyncio


PYTHON并发知识学习

学习资源:【2021最新版】Python 并发编程实战,用多线程、多进程、多协程加速程序运行

课程内容:

  1. 多线程:t=threading.Thread(target=func_name,args=(xx,))、t.start()、t.join()、queue.Queue、Lock、ThreadPoolEexcutor()
  2. 多进程:CPU密集型,使用多线程的方式,反而会拖慢速度。因为线程运行时,资源获取与释放消耗时间。原理:多核CPU并行。
  3. 多协程:异步IO-asyncio,超级循环+IO复用。

第一节 python并发编程简介

并发编程,缩短运行时间。
哪些程序提速的方法?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6mS1qPq-1658309193112)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713173202924.png)]
单线程串行:CPU-IO-CPI-IO如此一步一步执行

多线程并发:CPU与IO可以并行,IO的读取不需要CPU参与,实现并发。原理上仍是单CPU处理。

多CPU并行:多核CPU的电脑,可实现多任务CPU-IO执行,是真正的并行执行进行加速。

多机器并行:在多CPU并行的基础上,多机器进行任务运行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kyQFfsAG-1658309193115)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713175115605.png)]

第二节 怎样选择多线程多进程多协程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JXIConZL-1658309193117)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713175309432.png)]

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zicrfnO0-1658309193125)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713175253961.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aJvMtNp7-1658309193127)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713175509991.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7kmjJRU-1658309193129)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713181639954.png)]

第三节 python速度慢的罪魁祸首,全局解释器GIL

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QgAPwldM-1658309193132)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713181847925.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vg16L10n-1658309193133)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713182040585.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RdkTlenC-1658309193135)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713182251193.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wswKOkh4-1658309193136)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713182922415.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FJyALcRt-1658309193138)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713183349271.png)]

第四节 使用多线程,python爬虫被加速10倍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VouUledT-1658309193140)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713183440423.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qsP9POlB-1658309193142)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713183506514.png)]

import requests

urls = [f"http://www.cnblogs.com/#p{page}"
        for page in range(1, 50 + 1)]


def craw(url):
    r = requests.get(url)
    print(url, len(r.text))


craw(urls[0])

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import threading
import time

import note


def single_thread():
    print("single_thread begin")
    for url in note.urls:
        note.craw(url)
    print("single_thread end")


def muti_thread():
    print("multi_thread begin")
    threads = []
    for url in note.urls:
        threads.append(
            threading.Thread(target=note.craw, args=(url,))  # 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()
    muti_thread()
    end = time.time()
    print("multi_thread cost:", end - start, "seconds")

第五节 python实现生产者消费者爬虫

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PV81h7kc-1658309193143)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713191047745.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-98Ud15ss-1658309193144)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713191227612.png)]

生产者生产的结果,会通过中间数据,传给消费者。

生产者将“输入数据”作为原料,消费者将“输出数据”进行输出。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ryxsK6g-1658309193146)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713191240861.png)]

这架构共有两个processor

processor①:获取待爬取的URL,进行网页的下载

下载好的内容放在网页队列中

processor②:消费者消费网页的数据,进行解析与数据存储。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9jKlMXJ-1658309193148)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713191325564.png)]

queue.queue是阻塞函数,当队列已满,需要put填入数据。当队列没有元素,会等存在数据了再进行获取。

note.py文件

import requests
# 可以从html或xml文件中提取数据的python库
from bs4 import BeautifulSoup

urls = [f"http://www.cnblogs.com/#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")  # 获取html
    links = soup.find_all("a", class_="post-item-title")  # 文本解析
    # 获取url和标题文本
    return [(link["href"], link.get_text()) for link in links]


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

02.producer_consumer_spider.py文件

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import queue
import note
# 为了查看中间过程,①解析完后,进行sleep;②添加日志--主要是打印线程的名字
import time
import random
import threading


# 开始爬行,此为生产者方法,会有输入数据过程。
def do_craw(url_queue: queue.Queue, html_queue: queue.Queue):
    while True:
        url = url_queue.get()
        html = note.craw(url)
        html_queue.put(html)
        # 打印当前线程的名称、当前的url、队列的大小
        print(threading.current_thread().name, f"craw{url}", "url_queue.size=", url_queue.qsize())
        time.sleep(random.randint(1, 2))


def do_parse(html_queue: queue.Queue, fout):
    while True:
        html = html_queue.get()
        results = note.parse(html)
        for result in results:
            fout.write(str(result) + "\n")
        # 打印当前线程名称、总结果的大小、网站队列的大小。
        print(threading.current_thread().name, f"results.size", len(results), "html_queue.size=", html_queue.qsize())
        time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    """
    复杂的爬虫,可以分很多模块。
    每个模块,都可以使用不同的线程组进行处理。
    线程组之间,通过queue.Queue()进行交互。
    通过queue.Queue(),主线程将数据扔进去。
    url_queue数据传入生产者中后,产出html_queue。
    消费者获得html_queue后,产出数据至fout文件。
    """
    url_queue = queue.Queue()
    html_queue = queue.Queue()
    for url in note.urls:
        url_queue.put(url)
    for idx in range(3):
        t = threading.Thread(target=do_craw, args=(url_queue, html_queue), name=f"craw{idx}")
        t.start()
    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()

第六节 python线程安全问题以及解决方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FBGB1YRH-1658309193149)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713224515623.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KUKnxvA3-1658309193150)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713225357426.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GZ2jRg01-1658309193152)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220713225530214.png)]

#!/usr/bin/env python
# -*- coding: utf-8 -*-
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:
            # 添加此语句,一定会使tb运行失败----在if执行完后,就出现线程的切换,使得tb余额为-600。
            # 原因:sleep导致当前线程的阻塞,进行线程的切换。
            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)
    # 此处的name与threading.current_thread().name对应
    ta=threading.Thread(name="ta",target=draw,args=(account,800))
    tb=threading.Thread(name="tb",target=draw,args=(account,800))
    ta.start()
    tb.start()

第七节 python好用的线程池ThreadPoolExecutor

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TFJk5via-1658309193153)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715111526186.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d812BVOx-1658309193156)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715111837549.png)]

痛点:

新建线程系统需要分配资源,终止线程系统需要回收资源。如果可以重用线程,则可以减去新建/终止的开销。

解决办法:

线程池由2部分组成:线程池+队列。

  1. 线程池本身,里面是提前建好的线程。线程池中的资源会被重复地使用。
  2. 当新任务来了,不是直接加入线程池,而是加入任务队列。

流程:

  1. 线程池中已经创建完的线程,会挨个取出队列中的线程任务,进行依次的执行。
  2. 线程池中的任务执行完后,会接着取下一个任务进行执行。
  3. 当任务队列中没有新任务了,线程池不会销毁,等待下一个任务的到来。

总结:

通过任务队列与可重用的线程池,实现了线程池功能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ycoeyytO-1658309193157)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715113941779.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40ipHmFg-1658309193158)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715114000741.png)]

使用生产者线程组,实现map下的线程池写法。

使用消费者线程组,实现future下的两种线程池写法。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import concurrent.futures
import blog_spider

# craw
with concurrent.futures.ThreadPoolExecutor() as pool:
    htmls = pool.map(blog_spider.craw, blog_spider.urls)  # 注意,此处用的是urls,是一个列表
    htmls = list(zip(blog_spider.urls, htmls))  # 列表中的每个元素都是元组,元组中有url和html
    for url, html in htmls:
        print(url, len(html))

print("craw over")

# parse
with concurrent.futures.ThreadPoolExecutor() as pool:
    futures = {}
    # 方法一:顺序输出
    # 将future和url建立关系
    for url, html in htmls:
        future = pool.submit(blog_spider.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())

第八节 python使用线程池在web服务中实现加速

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WDEmZAMJ-1658309193159)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715131630708.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BiDKouqM-1658309193160)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715133231167.png)]

启动flask的方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PqelsVfm-1658309193163)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715133726689.png)]

flask默认写法:

app = flask.Flask(__name__)


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


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


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


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

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


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

使用线程池加速后的结果:

对象获取:pool.submit(read_file)

结果获取:result_file.result()

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import time
from concurrent.futures import ThreadPoolExecutor

import flask

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


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


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


def read_api():
    time.sleep(0.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()

几乎同时运行的话,就差不多300ms。总时间几乎为最长线程的时间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a91naA27-1658309193164)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715142947420.png)]

第九节 使用多进程multiprocessing模块加速程序的运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kaf4F5N1-1658309193165)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715143226219.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ptQQT7vg-1658309193166)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715144038828.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wLZ0yUE2-1658309193167)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715144140140.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ZzSpWmq-1658309193168)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715150633150.png)]

代码实现:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

PRIMES = [112272535095293] * 100


def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        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")

输出结果:

single_thread,cost: 44.58890986442566 seconds
multi_thread,cost: 46.927555561065674 seconds
multi_process,cost: 8.359080076217651 seconds

第十节 python在Flask服务中使用多进程池加速程序运行

和第9节类似的运行情况,但进程池的执行中出现报错。原因:他们的环境之间都是相互完全隔离的。有一点限制,当定义这个pool时,它所依赖的这些函数必须都已经声明完了。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import flask
from concurrent.futures import ProcessPoolExecutor
import math
import json

process_pool = ProcessPoolExecutor()
app = flask.Flask(__name__)


def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        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__':
    app.run()

博主建议将进程池的声明放在app.run()前面。但是我执行后,没有太大的区别。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jx0f3bxL-1658309193169)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715161845796.png)]

第十一节 python异步IO实现并发爬虫

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bFx9paEG-1658309193171)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715163002806.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8HpnaxEu-1658309193172)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220715163308206.png)]
在这里插入图片描述

步骤:

1、asyncio:至尊循环;get_event_loop():while…True超级循环。

2、async def myfunc(url)中,async:说明这个函数是一个协程。

await:代表IO,IO执行完毕后,不进行阻塞,直接进入下一个程序的执行asycio.get_event_loop()

3、批量创建task任务:loop.create_task,参数是具体的协程函数。

4、对tasks任务,放到loop.run_until_complete进行事件的执行,并等待完成。

总结:

整体来说,这个异步爬行的框架也是单线程执行的。但是他会多个URL的并发爬虫同时进行。

注意一点,异步IO编程中,依赖的库要支持await,也就是说支持异步IO特性。否则单线程就不能并发执行了。

协程是异步IO执行的函数。协程与普通函数的区别:协程需要超级循环来调度。

汇总:

异步IO实际是超级循环+IO多路复用的结合。

IO多路复用:IO时,CPU可以干其他事情。即当前任务处于IO时,CPU处理下一任务计算。

超级循环:当所有的任务都按照IO多路复用后,再返回来处理第一个任务剩余CPU需计算的任务。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import asyncio
import aiohttp
import blog_spider
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() # await不会一直等待,会切换到下一个url的爬取
            print(f"craw url:{url},{len(result)}")
# 协程是异步IO中执行的函数。他与普通函数的区别:协程需要超级循环进行调度。

# 获取超级循环
loop=asyncio.get_event_loop()
# 定义一个tasks列表
tasks=[
    loop.create_task(async_craw(url))
    for url in blog_spider.urls]
import time
start=time.time()
loop.run_until_complete(asyncio.wait(tasks))
end=time.time()
print("use time seconds:",end-start)
# 单线程异步爬虫,大部分情况下,是快于多线程的。
# 所以多线程的爬取时的并发,需要进行多线程不同调度的切换。本身是耗费时间的。
# 但单线程异步爬虫,没有切换的开销。所以执行起来相对来说是更快的。

第十二节 在异步IO中使用信号量控制爬虫并发度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUXlfhUP-1658309193174)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220718145828298.png)]

当线程完成一次对该semaphore对象的等待时,代表执行一次并发,所以计数值减一。

通过semephore控制并发度。当信号量满了后,会进入等待状态。防止爬虫将目标网站爬坏,超出他的处理能力。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k1QtLeVR-1658309193175)(C:\Users\Cheryl_Xu\AppData\Roaming\Typora\typora-user-images\image-20220718160255128.png)]

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值