逆向爬虫08 并发异步编程

逆向爬虫08 并发异步编程

我将引用我最喜欢的路飞学城林海峰Egon老师的讲课方式来复习这一节的内容。

1. 什么是并发异步编程?

这一部分的内容来源于计算机操作系统,如果想要详细了解并发异步等概念,最好去学一下操作系统这门课程,这里只是复习,总结和分享一下我所学到的内容。

要解释清楚并发异步编程,需要先弄明白如下这些词语的意思,如:并行,并发,同步,异步,阻塞,非阻塞。

并行:多个任务同时进行,可以理解为鸣人学习螺旋丸,多重影分身之后,所有的分身同时一起搓丸子,即真正意义上的同时进行。

并发:多个任务快速轮流切换运行,可以理解为时间管理大师,同时交往多个异性,但时间上并不冲突,通过快速切换来实现宏观意义上的同时进行。

同步:传统的任务都是同步的,意思就是所有任务整齐划一地排好队,一个挨着一个进入到CPU运行,只有上一个任务完成后,才能执行下一个任务。

异步:与同步相对,多个任务可以同时进行,异步的方式可以是并行,也可以是并发,只要宏观意义上是同时进行,就属于异步。

阻塞:当需要执行一个包含IO操作的任务时,操作系统会阻塞该任务,等IO操作完成后,再来继续执行该任务,阻塞的意思就是操作系统剥夺该任务的CPU使用权,将CPU分配给其他任务。

非阻塞:与阻塞相对,操作系统不会剥夺该任务的CPU使用权,如果执行IO操作时,IO已经准备好,则非阻塞任务执行成功,如果IO没有准备好,则直接告诉调用方执行失败。

知道了这些概念之后,应该就知道什么是并发异步编程了吧?指的就是微观上地多个任务快速切换,宏观上多个任务同时进行的一种编程方式,可以让程序的运行效率提高N倍。扯了这么多概念,对于非科班的小白来说,应该还是很难快速接受和掌握,下面我就再祭出经典的冯诺依曼体系结构图来说明一下,何为并发异步编程(其实我也是非科班的野路子,嘿嘿)。
在这里插入图片描述
上图就是冯诺依曼体系结构图,它告诉我们计算机分为五个部分,运算器,控制器,存储器,输入设备,输出设备。传统的同步编程中,当程序进入到IO操作时,操作系统为了提高硬件的使用效率,会阻塞进入IO的程序,将CPU分配给其他就绪的程序。任何一个程序,都需要通过输入设备,将数据输入到存储器中,由运算器来计算这些输入的数据,最后再通过输出设备输出计算结果,整个过程由控制器控制。通过上图可知,IO操作由输入和输出设备完成,计算由运算器完成,因此理论上这三个部分是可以同时工作的,有些程序过于简单,三个部分只能挨个进行。但如果一个程序中包含多个任务,且每个任务都包含一定的IO和计算操作,则可以通过合理地分配三者,达到同时运行的效果,最终从宏观上表现为好多好多任务同时进行的样子,这个合理分配的操作,也就是并发异步编程。

其实除了上述概念,如进程,线程,协程,这些概念其实都已经被人讲烂了,比如进程是操作系统资源分配的最小单位;线程是操作系统任务调度的最小单位;协程是单线程下,不涉及操作系统,通过软件自行调度的任务,由于不涉及到操作系统,因此速度比线程调度更快。这里我通过实验,来加强对进程概念的理解。平时我们都听说过2核4线程,4核8线程的CPU,CPU的核数表示可真正意义并行运行的任务数量。一般我们写的python程序只有一个进程,这个进程会被分配一个CPU的核,这也就是进程是资源分配的最小单位。我的电脑是8核的,因此每个核占12.5%的CPU效能,平时通过任务管理器查看CPU的使用率是1%,当我写一个python程序进行死循环做计算时,由于没有IO操作,操作系统就会分配一个核来全力支持它,不进行阻塞和切换,此时再观察任务管理器,就可以看到1%+12.5%≈14%左右。因此如果我们再开7个子进程,来执行相同计算操作的话,系统就会额外再分配7个核来支持,8核CPU就被分配完了,电脑会十分的卡顿,大家可以自行做这个实验,来对CPU和进程有更多的了解。关于线程,协程的实验,可以自行探索,如果发现合适且有趣的实验可以告诉我。

在这里插入图片描述

i = 0
while True:
    i += 1

在这里插入图片描述

2. 为什么要并发异步编程,什么时候适合并发编程?

那还用说,当然是速度快啊,那什么情况下适合通过并发异步编程来提高程序速度呢?通过前面的描述可知,并发异步编程就是合理分配不同的任务给输入输出设备和运算器,使这些设备可以分别为运行在不同阶段的任务服务,三者同时为一个程序进行服务,该程序的效率就会被提升。因此当一个程序的任务中包含很多离散的IO和计算操作时,就很适合并发异步编程,比如爬虫的多URL的网络请求,多文件的读写操作等。

3. 如何并发异步编程?

分为三种,1. 线程 2. 进程 3.协程,这里先复习线程和进程,协程后面再复习。

多线程写法一
"""
    1. 创建任务
    2. 实例化线程对象
    3. 启动任务
"""
from threading import Thread

# 创建任务
def func(name):
    for i in range(100):
        print(f"{i}. My name is {name}")

if __name__ == "__main__":
    # 实例化线程对象
    t1 = Thread(target=func, args=("JayChou",))
    t2 = Thread(target=func, args=("Apphao",))
    # 启动任务
    t1.start()
    t2.start()
多线程写法二
"""
    1. 自定义类继承Thread
    2. 通过__init__函数传参
    3. 重写run方法
"""
from threading import Thread

# 自定义类继承Thread
class MyThread(Thread):
    # 通过__init__函数传参
    def __init__(self, name):
        # 初始化时必须先调父类的初始化函数
        super(MyThread, self).__init__()
        self.name = name
    
    # 重写run方法
    def run(self):
        for i in range(100):
            print(f"{i}. My name is {self.name}")

if __name__ == "__main__":
    t1 = MyThread("JayChou")
    t2 = MyThread("Apphao")
    t1.start()
    t2.start()
线程池
线程池的好处是保护系统,防止程序无限制的开设线程,而导致资硬件源耗尽。
线程池的基本用法
"""
    1. 定义任务
    2. 开启线程池
    3. 提交任务
"""
from concurrent.futures import ThreadPoolExecutor

# 定义任务
def func(name):
    for i in range(10):
        print(f"{i}. My name is {name}")

if __name__ == "__main__":
    # 开启线程池
    with ThreadPoolExecutor(10) as t:
        for i in range(100):
            # 提交任务
            t.submit(func, f"JayChou {i}")
可以获得线程执行结果的写法
"""
    1. 定义任务
    2. 开启线程池
    3. 提交任务并添加回调函数
    4. 定义回调函数
"""
from concurrent.futures import ThreadPoolExecutor
import time

# 定义任务
def func(name, t):
    print(f"My name is {name}")
    time.sleep(t)
    return name

# 定义回调函数
def fn(res):
    print(res.result())

if __name__ == "__main__":
    # 开启线程池
    with ThreadPoolExecutor(3) as t:
        # 提交任务并添加回调函数
        t.submit(func, "JayChou", 2).add_done_callback(fn)
        t.submit(func, "Egon", 3).add_done_callback(fn)
        t.submit(func, "Apphao", 1).add_done_callback(fn)
add_done_callback()指定的回调函数在线程执行完成后会立即执行,由于每个线程的执行时间是不确定的,因此回调函数的执行顺序也是不确定的,返回值的顺序时不确定的,如果想要返回值顺序确定,则改用下面写法。
"""
    1. 定义任务
    2. 开启线程池
    3. 通过map提交任务,并接受map的返回值
    4. for循环获取map的返回值
"""
from concurrent.futures import ThreadPoolExecutor
import time

# 定义任务
def func(name, t):
    print(f"My name is {name}")
    time.sleep(t)
    return name

if __name__ == "__main__":
    # 开启线程池
    with ThreadPoolExecutor(3) as t:
        result = t.map(func, ["JayChou", "Egon", "Apphao"], [2, 3, 1])
        # map的返回值时生成器,通过for循环取出里面的内容
        for r in result:
            print(r)
多进程写法一:和多线程几乎一摸一样
"""
    1. 创建任务
    2. 实例化进程对象
    3. 启动任务
"""
from multiprocessing import Process

# 创建任务
def func(name):
    for i in range(100):
        print(f"{i}. My name is {name}")

if __name__ == "__main__":
    # 实例化进程对象
    p1 = Process(target=func, args=("JayChou",))
    p2 = Process(target=func, args=("Apphao",))
    # 启动任务
    p1.start()
    p2.start()
多进程写法二:和多线程几乎一摸一样
"""
    1. 自定义类继承Process
    2. 通过__init__函数传参
    3. 重写run方法
"""
from multiprocessing import Process

# 自定义类继承Process
class MyProcess(Process):
    # 通过__init__函数传参
    def __init__(self, name):
        # 初始化时必须先调父类的初始化函数
        super(MyProcess, self).__init__()
        self.name = name
    
    # 重写run方法
    def run(self):
        for i in range(100):
            print(f"{i}. My name is {self.name}")

if __name__ == "__main__":
    p1 = MyProcess("JayChou")
    p2 = MyProcess("Apphao")
    p1.start()
    p2.start()
进程池
进程池的基本用法,和线程池几乎一模一样
"""
    1. 定义任务
    2. 开启进程池
    3. 提交任务
"""
from concurrent.futures import ProcessPoolExecutor

# 定义任务
def func(name):
    for i in range(10):
        print(f"{i}. My name is {name}")

if __name__ == "__main__":
    # 开启进程池
    with ProcessPoolExecutor(10) as t:
        for i in range(100):
            # 提交任务
            t.submit(func, f"JayChou {i}")
获取进程的返回值,和线程池几乎一模一样
"""
    1. 定义任务
    2. 开启进程池
    3. 提交任务并添加回调函数
    4. 定义回调函数
"""
from concurrent.futures import ProcessPoolExecutor
import time

# 定义任务
def func(name, t):
    print(f"My name is {name}")
    time.sleep(t)
    return name

# 定义回调函数
def fn(res):
    print(res.result())

if __name__ == "__main__":
    # 开启进程池
    with ProcessPoolExecutor(3) as t:
        # 提交任务并添加回调函数
        t.submit(func, "JayChou", 2).add_done_callback(fn)
        t.submit(func, "Egon", 3).add_done_callback(fn)
        t.submit(func, "Apphao", 1).add_done_callback(fn)
有序地获得进程池中返回值,和线程池几乎一模一样
"""
    1. 定义任务
    2. 开启进程池
    3. 通过map提交任务,并接受map的返回值
    4. for循环获取map的返回值
"""
from concurrent.futures import ProcessPoolExecutor
import time

# 定义任务
def func(name, t):
    print(f"My name is {name}")
    time.sleep(t)
    return name

if __name__ == "__main__":
    # 开启进程池
    with ProcessPoolExecutor(3) as t:
        result = t.map(func, ["JayChou", "Egon", "Apphao"], [2, 3, 1])
        # map的返回值时生成器,通过for循环取出里面的内容
        for r in result:
            print(r)
何时使用多线程,何时使用多进程?
1. 多线程:任务相对统一,互相特别相似
2. 多进程:多个任务相互独立,很少有交集
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值