一.前言
并发编程在我们实际开发中是十分重要的,例如说,在爬虫请求的时候会遇到十分多的IO阻塞,这时候并发就能节约大量的时间
二.进程、线程与协程
2.1 进程的概念
我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。
进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序、数据集合和进程控制块三部分组成。
例子:我和我的女朋友们的故事
我就是CPU,我跟三个女朋友玩就是三个任务
1. 我教第一个女朋友做菜,菜谱就是程序,食材就是数据,我做饭的过程就是一个进程(切换,状态保存)
2. 我给第二个女朋友治疗脚伤,医疗手册就是程序,医药箱就是数据,治疗脚伤的过程就是第二个进程
。。。
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
2.2 线程的概念
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
线程的生命周期
在单个处理器运行多个线程时,并发是一种模拟出来的状态。操作系统采用时间片轮转的方式轮流执行每一个线程。现在,几乎所有的现代操作系统采用的都是时间片轮转的抢占式调度方式,如我们熟悉的Unix、Linux、Windows及macOS等流行的操作系统。
我们知道线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操作系统中,进程有五种状态,创建、就绪、运行、阻塞(等待)、退出。早期的进程相当于现在的只有单个线程的进程,那么现在的多线程也有五种状态,现在的多线程的生命周期与早期进程的生命周期类似。
线程的生命周期
创建:一个新的线程被创建,等待该线程被调用执行;
就绪:时间片已用完,此线程被强制暂停,等待下一个属于它的时间片到来;
运行:此线程正在执行,正在占用时间片;
阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;
退出:一个线程完成任务或者其他终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。
进程与线程的区别
前面讲了进程与线程,但可能你还觉得迷糊,感觉他们很类似。的确,进程与线程有着千丝万缕的关系,下面就让我们一起来理一理:
线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
调度和切换:线程上下文切换比进程上下文切换要快得多。
2.3 协程(Coroutines)
协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。因为是自主开辟的异步任务,所以很多人也更喜欢叫它们纤程(Fiber
),或者绿色线程(GreenThread
)。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
Tips:协程解决的是线程的切换开销和内存开销的问题
优点: 这种模型的好处是线程上下文切换都发生在用户空间,避免的模态切换(mode switch),从而对于性能有积极的影响。
三.多线程的实现
3.1 threading模块
我们先来介绍一下threading的基本使用
t = threading.Thread(target=spider01, args=(3,)) #target后面跟函数名字,arges后面跟函数的参数,这个代表创建了一个子线程t1
t.start() #.start() 代表线程进行了就绪状态
t.join() #.join() 代表阻塞等待,要等待t1结束了才会执行其他的线程
Python提供两个模块进行多线程的操作,分别是thread
和threading
,前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。
实例:串行版本
发现串行版本整个程序一共执行了八秒多
实例:多线程版本
import threading
import time
def spider01(timer):
print("spider01 start")
time.sleep(timer) # 模拟IO
print("spider01 end")
def spider02(timer):
print("spider02 start")
time.sleep(timer) # 模拟IO
print("spider02 end")
start = time.time()
# 创建线程对象
t1 = threading.Thread(target=spider01, args=(3,))
t1.start()
t2 = threading.Thread(target=spider02, args=(5,))
t2.start()
t1.join()
t2.join()
end = time.time()
print("时间花销:", end - start)
这里首先我们得弄清楚一个事,这里一共有几个进程,不少人可能会说有两个,但是其实是三个,还有一个是主线程,我们弄清楚这个就很关键,那我们运行一下
发现这次执行时间就只有5s,并且spyder01执行后,遇到阻塞了,就立刻执行下一个函数,这两个函数并驾齐驱,一共花费5s,但是这里我如果把两个join给去掉会发生什么呢
发现这个就只花费了0.001秒,怎么可能呢,这是因为一个有三个线程,t1线程开始了,遇到阻塞了,立刻进入t2这个线程,t2也阻塞了,而我们没有进行阻塞等待,所以直接执行主线程,主线程一共花费0.001秒结束,所以要时刻记得主线程,要不然使用经常会忘记子线程join阻塞,就会导致各种bug
3.2 多线程并发的cs架构
之前我们讲过网络编程,但是当时我们说了,我们那个只能开一个客户端,多开客户端就会卡住,这是因为没有开多线程,那通过threading模块我们是不是就能开多线程并发,让他支持多个客户端,我们要管的肯定就服务端,那我们应该怎么修改服务端呢
那我们首先就想我们要在哪里修改,那我们就得先知道,他为什么只能连接一个客户端,这里我就不给大家展示了,大家可以多开几个客户端就可以发现,每次三个客户端都能连接成功,但是输入值都不能返回,那是不是就是第二个while里面的阻塞函数,就和谈恋爱一样,conn是你的对象,三个客户端连接上就给你三个conn对象,然后呢,第一个对象给你发消息,发一个你回一个,第二个和第三个对象给你发消息的时候,你都在等着你第一个对象的消息,这样非常的不好!这样是不是就冷落了你的第一个对象,那我们就要多线程处理,让你在等待第一个对象发消息的时候,也能回你的第二三个对象,这我们只需要给第二个while封装成一个函数就好,然后异步支持。
我们这样就好了,这样就能实现功能了,但是这里我要问大家,要不要用t.join(),加入我们用了,他是不是就要等待函数里面的while 1结束,才能执行主进程里的main了,这样第二个女朋友conn不还是连不上了吗,大家细细品味一下这里,所以说join也不能乱加
import socket
from loguru import logger
import threading
def conn_handle(conn):
while 1:
# (3) 收消息
data_bytes = conn.recv(1024) # 阻塞函数
print("data:", data_bytes.decode())
# len(data_bytes) == 是为了处理意外退出的
if data_bytes == "quit".encode() or len(data_bytes) == 0:
logger.info(f"来自于{addr}客户端退出!")
break
# (4) 处理数据并发送给客户端
data = data_bytes.decode()
res = data.upper()
conn.send(res.encode())
# 构建服务端套接字对象
sock=socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) #这个是选择tcp协议
# 服务端三件套:bind listen accept
sock.bind(("127.0.0.1", 8899))
sock.listen(5)
logger.info("服务器启动")
while 1:
logger.info("等待新连接...")
conn, addr = sock.accept() # 阻塞函数
# print(f"conn:{conn},addr:{addr}")
logger.info(f"来自于客户端{addr}的请求成功")
t=threading.Thread(target=conn_handle,args=(conn,))
t.start()
3.3 线程池
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统的交互。在这种情形下,使用线程池可以很好地提升性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
此外,使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。
这里运用ThreadPoolExecutor模块,这里我来介绍一下这个模块的基本使用
from concurrent.futures import ThreadPoolExecutor #导入模块
pool = ThreadPoolExecutor(3) #创建线程池,这里3代表最多容纳三个
future = pool.submit(task, 1) #提交任务 task代表函数名,1代表参数是1 这里不用传元组形式,和threading有区分
pool.shutdown() # 阻塞等待
这里给出代码就好
import time
from concurrent.futures import ThreadPoolExecutor
def task(i):
print(f'任务{i}开始!')
time.sleep(i)
print(f'任务{i}结束!')
return i
start = time.time()
pool = ThreadPoolExecutor(2)
future01 = pool.submit(task, 1)
# print("future01是否结束", future01.done())
# print("future01的结果", future01.result()) # 同步等待
future02 = pool.submit(task, 2)
future03 = pool.submit(task, 3)
pool.shutdown() # 阻塞等待
print(f"程序耗时{time.time() - start}秒钟")
print("future01的结果", future01.result())
print("future02的结果", future02.result())
print("future03的结果", future03.result())
3.4 互斥锁
并发编程中需要解决一些常见的问题,例如资源竞争和数据同步。由于多个线程或进程可以同时访问共享的资源,因此可能会导致数据不一致或错误的结果。为了避免这种情况,需要采用合适的同步机制,如互斥锁、信号量或条件变量,来确保对共享资源的访问是同步和有序的。
这里我直接给大家举个例子
这个程序就是一个相当于异步给100依次减少到1,按道理结果肯定是0对不对,但是我们一旦运行
发现结果是86,这个是为什么呢,我们仔细想想,异步的话,每次减少是不是可能几个线程同时拿一个值进行减少1,这样就会导致结果不对,那我们如何解决呢,有的哥们可能说那不异步就好了,那如果其他地方还有大量IO操作呢,那我们岂不是眼睁睁的看着他浪费时间而无能为力,当然不是这样的,这里很简单,只需要使用我们的互斥锁就好
这里先给大家介绍一下锁的基本用法
Lock = threading.Lock() #创建锁
Lock.acquire() #上锁
Lock.release() #放锁
我们只需要加上这些代码就好
import time
import threading
Lock = threading.Lock()
def addNum():
global num # 在每个线程中都获取这个全局变量
# 上锁
Lock.acquire()
t = num - 1
time.sleep(0.0001)
num = t
Lock.release()
# 放锁
num = 100 # 设定一个共享变量
thread_list = []
for i in range(100):
t = threading.Thread(target=addNum)
t.start()
thread_list.append(t)
for t in thread_list: # 等待所有线程执行完毕
t.join()
print('Result: ', num)
这样不管我们怎么运行,结果都正确了
3.5 队列(queue)
1.队列的基本用法
这里就介绍一个数据结构,队列,学过数据结构的都知道,队列是一种数据结构,遵循着FIFO(先进先出)原则,而python他不用像c语言那么定义,而是封装一个模块queue,我们直接用就好,我们如果没接触过数据结构可以把他就当成是列表,字典这种数据,但是他和字典列表这些的最大区别就是他线程安全,他自己天然就有一把锁,不会出现我们之前的那种情况。
queue模块的使用
q = queue.Queue(3) #创建指定大小的队列
q.put(100) #把值放入队列
q.get() #取队列的值,取出后值就从队列中去除
q.empty() #判断队列是否为空
q.qsize() #返回队列中剩余元素的个数
这些里面都定义了blocak=True 当我们使用blocak=False的时候 就会不会是阻塞机制,而是报错机制
大概演示一下就是这样
2.生产者-消费者模型
这个是我们需要重点了解的一个概念
常见的线程队列模型是生产者-消费者模型。生产者线程负责生成数据并将其放入队列,而消费者线程则从队列中获取数据并进行处理。通过使用队列作为缓冲区,生产者和消费者之间解耦,可以实现高效的线程间通信。
举例:就好比我们点外卖的时候,我们不会说把一个逻辑都写在一个函数里面,而是通过生产者消费者进行区分开来,比如用户点外卖,会提交给一些重要的信息,比如下单时间,订单金额,位置信息等等,而我们提交完之后不会立刻进行处理,而是把他提交给队列,然后在写一套处理订单的逻辑,来一个任务提交一个任务,另外一个逻辑就是处理完一个任务就拿下一个任务,这样可以增加程序的耦合性
import queue
import time
import threading
q = queue.Queue()
def producer():
for i in range(1, 11):
time.sleep(3)
q.put(i)
print(f"生产者生产数据{i}")
print("生产者结束")
def consumer(name):
while 1:
val = q.get()
print(f"消费者{name}消费数据:{val}")
time.sleep(6)
if val == 10:
print("消费者结束")
break
p = threading.Thread(target=producer)
p.start()
time.sleep(1)
c1 = threading.Thread(target=consumer, args=("消费线程1",))
c1.start()
c2 = threading.Thread(target=consumer, args=("消费线程2",))
c2.start()
这个就是生产者消费者模型
四.多进程实现
4.1 多进程处理计算密集型任务
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
这句话总结起来的意思就是,因为GIL锁的原因,python处理多线程并不能同时给多个cpu,因为他一个进程只能给一个cpu,而如果我们想要充分利用cpu,python里面就必须得开多进程,但是多进程的话,他的坏处就是占用空间大,而进程我们并不能同时开很多个,所以多进程我们是来解决多核处理的,我们要使用一个multiprocessing模块
t = multiprocessing.Process(target=foo, args=()) 3创建一个进程对象
t.start() #代表进程进行了就绪状态
t.join() #等待进程执行完毕
是不是和我们的threading的用法差不多,对的,但是他必须得用if __name__ == '__main__':这个语法
大家可能会说,多进程利用多核有什么用呢,感觉用处不大啊,但是我要告诉大家,在进行计算密集型任务的时候,多进程的用处就大的离谱了,我们回想讲bl锁的时候,是不是我们在计算的时候我们那个计算时间压根少不了,如果少了结果不就不对了,我们多cpu就能同时处理这么多,这里给大家大数据计算的例子
我们来给这个大的数据循环自加1
串行版本
发现执行三个串行版本是九秒多
多线程版本
发现也是九秒多,并且时间还长了一点,因为来回切换线程还会浪费时间,所以时间更长了些,这里没有加锁是因为这里没有io阻塞,如果有的话还是要加上的
多进程版本
发现多进程就只需要3秒多了,相当于执行一个的时长,如果我们开多个进程,还是三秒,当然这个得要求电脑有那么多核
import multiprocessing
import threading
import time
def foo(x):
ret = 1
for i in range(x):
ret += i
print(ret)
start = time.time()
# (1) 串行版本
# foo(120000000)
# foo(120000000)
# foo(120000000)
# end = time.time()
# print('totle time:',end - start)
# (2) 多线程版本
# t1 = threading.Thread(target=foo, args=(120000000,))
# t1.start()
# t2 = threading.Thread(target=foo, args=(120000000,))
# t2.start()
# t3 = threading.Thread(target=foo, args=(120000000,))
# t3.start()
#
# t1.join()
# t2.join()
# t3.join()
#
# end = time.time()
# print('totle time:',end - start)
# (3) 多进程版本
if __name__ == '__main__':
p1 = multiprocessing.Process(target=foo, args=(120000000,))
p1.start()
p2 = multiprocessing.Process(target=foo, args=(120000000,))
p2.start()
p3 = multiprocessing.Process(target=foo, args=(120000000,))
p3.start()
p1.join()
p2.join()
p3.join()
end = time.time()
print('totle time:',end - start)
这个代码给大家,大家也可以测试一下
五.协程并发
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
5.1 asyncio的基本使用
asyncio即Asynchronous I/O是python一个用来处理并发(concurrent)事件的包,是很多python异步架构的基础,多用于处理高并发网络请求方面的问题。
为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法==async==和==await==,可以让coroutine的代码更简洁易读。
asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等。
asyncio 往往是构建 IO 密集型和高层级 结构化 网络代码的最佳选择。
旧版本用法
loop = asyncio.get_event_loop() #创建事件循环对象
tasks = [task(1), task(2)] #将协程对象加到事件循环中
asyncio.wait(tasks) #将协程对象进行收集
loop.run_until_complete(asyncio.wait(tasks)) #阻塞调用,直到协程全部运行结束才返回
loop.close() 事件结束
import asyncio
import time
async def task(i):
print(f"task {i} start")
await asyncio.sleep(1)
print(f"task {i} end")
start = time.time()
# 创建事件循环对象
loop = asyncio.get_event_loop()
# 直接将协程对象加入时间循环中
tasks = [task(1), task(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = time.time()
print("cost timer:", end - start)
这个是旧版本的解释器用的,一会会和大家讲新版本的
5.2 asyncio的任务对象
task=asyncio.ensure_future(work(1, 1) #构建一个future对象 work是函数
task.done() #任务是否完成
task.result() #打印执行完的结果
tasks[0].add_done_callback() #执行完的回调函数,参数写函数,那个函数里面必须得有一个obj,作为完成的形参接收
import asyncio, time
def task01_callback(obj):
print("任务1执行完成")
print(obj.done(),obj.result())
async def work(i, n): # 使用async关键字定义异步函数
print('任务{}等待: {}秒'.format(i, n))
await asyncio.sleep(n) # 休眠一段时间
print('任务{}在{}秒后返回结束运行'.format(i, n))
return i + n
start_time = time.time() # 开始时间
tasks = [asyncio.ensure_future(work(1, 1)),
asyncio.ensure_future(work(2, 2)),
asyncio.ensure_future(work(3, 3))]
tasks[0].add_done_callback(task01_callback) #回调函数
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
print('运行时间: ', time.time() - start_time)
for task in tasks:
print('任务执行结果: ', task.result())
5.3 新版本语法支持
async.create_task()
创建task
async.gather()
获取返回值
async.run()
运行协程
import asyncio, time
async def work(i, n): # 使用async关键字定义异步函数
print('任务{}等待: {}秒'.format(i, n))
await asyncio.sleep(n) # 休眠一段时间
print('任务{}在{}秒后返回结束运行'.format(i, n))
return i + n
async def main():
tasks = [asyncio.create_task(work(1, 1)),
asyncio.create_task(work(2, 2)),
asyncio.create_task(work(3, 3))]
# 将task作为参数传入gather,等异步任务都结束后返回结果列表
response = await asyncio.gather(tasks[0], tasks[1], tasks[2])
print("异步任务结果:", response)
start_time = time.time() # 开始时间
# 新版不用创建loop 事件循环对象了
asyncio.run(main())
print('运行时间: ', time.time() - start_time)
新版语法不用创建loop了,他内部已经完成了,而且主逻辑必须在asynico.run里面执行,我们只需要记住新版本语法就行了
5.4 基于协程的异步爬虫
目标网站,这个网站没有逆向,我们可以直接爬取,我们从第二页开始爬取
https://pic.netbian.com/4kmeinv/index_2.html
同步版本
import os.path
import time
import requests
import re
def get_page_img_urls(page):
# 获取页面内容
res = requests.get(f"https://pic.netbian.com/4kmeinv/index_{page}.html")
# 使用正则表达式提取图片URL
ret = re.findall('<img src="(/uploads/allimg/.*?)"', res.text)
print(ret)
return ret
def download_one_img(url, n):
# 下载单张图片
res = requests.get(url)
f = open(f"./imgs/{n}", "wb")
f.write(res.content)
f.close()
print(f"{n}下载成功")
def download_page_imgs(img_urls):
domain = "https://pic.netbian.com/"
for path in img_urls:
title = os.path.basename(path)
url = domain + path
download_one_img(url, title)
def main():
start = time.time()
for i in range(2, 6):
page_img_urls = get_page_img_urls(i)
# 获取页面中的图片URL列表
download_page_imgs(page_img_urls)
# 下载页面中的所有图片
end = time.time()
print("cost timer:", end - start)
main()
总共花费四十多秒,这也太慢了吧
异步版本
这里我们发送请求就不能用requests模块了,而是要用 asyncio模块发送请求,和requests使用有所区别
async with aiohttp.ClientSession() as session:
async with session.get(url, verify_ssl=False) as res:
data = await res.content.read()
#这个发送请求就这个用法
我们异步下载文件就不能用简单的with open了,而是要用aiofiles模块,这个模块得下载,这里给出一个下载的实例代码
async def write_file():
async with aiofiles.open('file.txt', 'w') as file:
await file.write('Hello, World!')
爬虫代码
import time
import requests
import re
import asyncio
import aiohttp
import os
import aiofiles
async def get_page_img_urls(page):
# 获取页面内容
# res = requests.get(f"https://pic.netbian.com/4kmeinv/index_{2}.html")
async with aiohttp.ClientSession() as session:
async with session.get(f"https://pic.netbian.com/4kmeinv/index_{page}.html", verify_ssl=False) as res:
data = await res.content.read()
# 使用正则表达式提取图片URL
ret = re.findall('<img src="(/uploads/allimg/.*?)"', data.decode("GBK"))
print(ret)
return ret
async def download_one_img(url, n):
# 下载单张图片
async with aiohttp.ClientSession() as session:
async with session.get(url, verify_ssl=False) as res:
data = await res.content.read()
async with aiofiles.open(f"./imgs/{n}", "wb") as file:
await file.write(data)
print(f"{n}下载成功")
async def download_page_imgs(img_urls):
domain = "https://pic.netbian.com/"
for path in img_urls:
title = os.path.basename(path)
url = domain + path
await download_one_img(url, title)
async def main():
start = time.time()
for i in range(2, 6):
# 获取页面中的图片URL列表
page_img_urls = await get_page_img_urls(i)
# 下载页面中的所有图片
await download_page_imgs(page_img_urls)
end = time.time()
print("cost timer:", end - start)
asyncio.run(main())
而异步下载就只需要二十秒,这样是不是快了很多
六.总结
今天讲了python并发的知识,其实最重要的就是后面的协程,我们这个需要掌握,而前面的线程和进程了解即可,在我讲协程的时候大家应该已经感觉的到了,并且python对协程的支持是可以的,所以大家可以着重看看这里
七.补充
有什么问题私我,记得点赞关注加收藏哦,有求必应