基础概念
CPU密集型和IO密集型
cpu密集型任务的特点是:持续使用cpu,持续不断的运行。例如压缩与解压,代码编译,视频编解码等。这些任务只要开始,cpu占用曲线就居高不下。

IO密集型任务特点是:大量时间用于等待,cpu处理时间少。例如用户输入,文件I/O,网络I/O,数据库I/O等。cpu占用曲线很低,偶尔有尖峰。
那如何解决这个问题?
并发和并行
并发是物理上一个cpu交替做,一个cpu通过控制执行时间即可做到(时间片轮转算法)
比如咖啡店做咖啡,一个咖啡机,轮流做拿铁,卡布奇诺,美式,对于客户来说,看起来几乎在同时处理
并行是物理上多个cpu同时做,因此必须多核CPU才能实现并行
比如咖啡店做咖啡,3台机器,分别做拿铁,卡布奇诺,美式,互不干扰,同时完成

进程到底有哪些东西构成?线程有哪些东西构成?
进程在操作系统内核角度的描述
| 组成部分 | 内容 |
|---|---|
pid |
进程唯一编号 |
| 进程状态 | 就绪,运行,阻塞 |
程序计数器(pc) |
执行到哪条指令了 |
| 代码段 | 程序的指令 |
| 数据段 | 全局变量、静态变量 |
| 堆 | 动态分配的对象(malloc) |
| 栈 | 函数调用链 + 局部变量 |
文件描述符表(fd) |
打开的文件和网络连接file:[0,1,2,3],标准输入,标准输出和标准错误是哪几个文件描述符 |
线程相较于进程独立的东西(TID,pc,线程状态,栈区)剩下的数据段,代码段,堆等都与进程共享。
在 Linux 内核的代码里,其实根本没有线程这个数据结构。无论是进程还是线程,在内核中都叫“任务”(Task),都用task_struct结构体来表示。
那进程和线程的区别体现在哪?—— 体现在“共享资源的多少”上。
可以把 task_struct理解为一张“任务身份证”,每个执行流都有一张。区别在于:
一个进程(通常指只有主线程的情况):这张身份证指向的所有资源(内存、文件等)都是独占的。
一个线程:这张身份证指向的资源,大部分和同组的其他身份证共享
这就是为什么 Linux 的线程被称为轻量级进程的原因创建线程时,内核不需要像创建进程那样复制一大堆资源,只需要新建一个 task_struct,然后让它指向现有资源即可。
根本区别:操作系统资源分配的最小单位是进程,CPU独立调度和执行的最小单位是线程。
共享关系:进程间内存相互独立,不可直接访问;同一进程下的线程,共享该进程的堆,文件等资源。
切换开销:进程更大,线程更小,只要保存少量寄存器和栈信息。
进程转换状态图
见课件部分,核心三态,补充两态
线程和进程一样
就绪缺的是cpu时间,阻塞缺的是某个资源或者事件(磁盘,网络,用户输入)

同步和异步、阻塞与非阻塞
| 概念 | 描述的对象 | 描述的是什么 | 例子 |
|---|---|---|---|
| 阻塞/非阻塞 | 调用者(线程/进程)的状态 | 调用者在等待结果时,自己还能不能干别的事 | (阻塞)站在排队队伍里,不能刷手机,不能看书,整个人处于“石化”和等待状态,只能干等。(非阻塞)在等餐时,你一直在刷手机、背单词或者和同学聊天。虽然你还在等,但你并没有被“冻结”,然后自己轮询看看好没好(polling) |
| 同步/异步 | 被调用者(任务/IO,通常是函数)的通知方式 | 任务完成时,是主动通知调用者,还是需要调用者主动去问 | (同步)你在打饭窗口点了小笼包,阿姨正在蒸。你就在窗口一直盯着阿姨,直到阿姨把小笼包递给你,你才离开(异步) 你在取餐窗口点了黄焖鸡,阿姨给了你一个会震动的取餐号牌(回调机制)。你拿到号牌就可以离开窗口了,等黄焖鸡好了,号牌一震动,你再去取 |
阻塞/非阻塞 = 我(调用者)还能不能动
同步/异步 = 它(被调用者)会不会叫我
排列组合
| 组合 | 调用者状态 | 通知方式 |
|---|---|---|
| 同步阻塞 | 等结果时不能动 | 调用者主动等 |
| 同步非阻塞 | 立即返回,但要轮询 | 调用者主动问 |
| 异步(非阻塞) | 立即返回,不用轮询 | 被调用者主动通知(回调) |
==总结==:
现代操作系统运行程序时,基本都是以并发方式进行的。这里的“并发”指的是在宏观上多个程序同时向前推进,但在微观(单核 CPU)上其实是交替执行,每个时刻只有一个程序占用 CPU。
为了更精确地说明,有必要区分两个概念:
- 并发:多个任务在同一个时间段内交替执行(逻辑上同时)。单核 CPU 通过时间片轮转实现并发。
- 并行:多个任务在同一时刻同时执行(物理上同时)。只有多核 / 多 CPU 才能实现真正并行。
为什么操作系统运行程序基本都是并发的?
- CPU 数量有限 即使是普通的多核 CPU(例如 8 核),同时运行的线程数往往远超 8 个。操作系统必须通过时分复用让所有线程都有机会运行。
- 提高资源利用率 一个程序等待 I/O(磁盘、网络、键盘)时,CPU 会空闲。并发调度允许此时运行另一个程序,避免 CPU 空转。
- 用户交互需求 现代操作系统需要同时响应鼠标、键盘、网络包、后台任务等,必须用并发来提供流畅体验(例如边听歌边写文档)。
- 进程 / 线程模型本身就是为并发设计的 操作系统提供进程、线程作为基本执行单元,调度器负责在它们之间快速切换(通常每秒数十到数千次),宏观上就像“同时运行”。
并发是如何实现的
核心机制是抢占式多任务(现代操作系统普遍采用):
时间片轮转:每个线程获得一个很短的 CPU 时间片(通常 10~100 ms)。时间片用完或主动阻塞(如 sleep、wait I/O)时,触发调 度。
时钟中断:硬件定时器周期性中断 CPU,强制剥夺当前线程,并交由调度器选择下一个线程。
优先级调度:重要任务(如实时音频)可抢占普通任务的 CPU 时间。
这样,即使你只打开了一个文本编辑器,后台依然有数十个系统服务、驱动程序、守护进程在并发执行。
并发 ≠ 并行 单核时代:严格并发(交替执行),没有真正意义的并行。
多核时代:可以同时存在并发 + 并行。 例如 4 核 CPU 上运行 100 个线程:同一时刻有 4 个线程在并行运行,整体上仍然是 100 个线程并发运行。
面试题
一、同步非阻塞 vs 异步回调 核心对比
| 维度 | 同步非阻塞 | 异步回调 |
|---|---|---|
| 操作完成通知方式 | 调用方主动轮询 | 系统自动调用回调 |
| CPU开销 | 有轮询开销(可结合epoll优化) |
无轮询开销 |
| 典型代表 | epoll + 非阻塞socket(Reactor) | Node.js、io_uring(Proactor) |
| 适用场景 | 活跃连接数可控、需精细控制 | 极高并发、突发流量、长尾敏感 |
二、关键追问答案
- 谁性能更好?
理论异步更优,但实际
epoll(同步非阻塞)已足够高效。极高IOPS下io_uring(真正异步)胜出。 epoll是同步非阻塞还是异步? 同步非阻塞。事件就绪后需应用主动调用read/write完成I/O。- 如何解决回调地狱?
Promise链式调用 →async/await语法糖(本质仍基于回调,但扁平化)。 - 工作中如何选?
- 高性能中间件(网关、代理)→ 同步非阻塞+
epoll(如Nginx) - 业务密集型应用 → 异步回调+
async/await(如Node.js、Pythonasyncio) - 低并发简单场景 → 同步阻塞最省事
- 高性能中间件(网关、代理)→ 同步非阻塞+
三、面试结语(核心结论)
同步非阻塞需要应用主动轮询或依赖事件通知,实际读写由用户线程完成,属于同步范畴;异步回调由系统/线程池完成后反向通知应用,属于异步范畴。 现代开发中,异步回调配合
async/await是主流范式,但同步非阻塞(Reactor)在底层基础库中仍广泛使用。
python多进程
1. 进程创建和基础结构
进程核心方法:
start——启动进程
join——主进程等待子进程终止。默认主进程会自动等待所有子进程结束才会退出
os. getpid( )——获取进程id
os.getppid()——获取父进程id
import os
from multiprocessing import Process
from time import sleep
def func(name):
print(f"以子进程方式运行{name}")
print(f"子进程进程号={os.getpid()}")
print(f"子进程中父进程进程号={os.getppid()}")
sleep(10)
# linux/mac系统是fork机制,windows用的是spawn机制,子进程为了获取代码,必须把Python文件从头导入一边
if __name__ == "__main__":
p = Process(target=func, args=("test",)) # 单元素元组需要加,否则和字符串混淆了
p.start() # 一定用start才能开启新进程,run只是在当前线程直接运行函数
# p.join() # 等待子进程结束
print(f"父进程进程号={os.getpid()}")
print("主进程运行结束")



2. 多进程同时创建
import time
from multiprocessing import Process
def func(name):
print("hello %s" % name)
time.sleep(1)
if __name__ == "__main__":
for i in range(20):
p = Process(target=func, args=(f"{i}",))
p.start()
# p.join() # 如果在每个start之后,都join等待子进程,便可以保证顺序运行,但是,这无法发挥多进程优势
print("主程序结束,等待子进程....") # 运行结果发现,进程调度确实不是顺序的,由操作系统自己决定

3. 跟随进程
随着主进程结束,子进程就被立即强行终止,因此此类进程并不保证里面的任务能跑完。只要主进程代码执行完,daemon=True的子进程立即杀死。
import time
from multiprocessing import Process
def foo():
print(123)
time.sleep(1)
print("end123") # 父进程代码执行结束,所以这里不会输出
def bar():
print(456)
time.sleep(3)
print("end456")
if __name__ == "__main__":
p1 = Process(target=foo, daemon=True) #设置为跟随,主进程退出,p1立即被强制终止
p2 = Process(target=bar)
p1.start()
p2.start()
time.sleep(0.1)
print("main-------")
适合心跳、日志刷写这种丢了也没啥事的任务
操作系统中的守护进程不是python中的这个含义,可以去检索一下,linux操作系统守护进程如何创建,如何查询,如何关闭
4. 进程间通信(IPC)
Value共享
from multiprocessing import Process, Value
def increment(num):
num.value += 1
print(num.value)
if __name__ == "__main__":
counter = Value("i", 0) # 'i' 表示整数类型
processes = [Process(target=increment, args=(counter,)) for _ in range(30)]
for p in processes:
p.start()
for p in processes:
p.join()
# 每次执行的结果都会不一样
Array共享
from multiprocessing import Array, Process
def increment(arr):
for i in range(len(arr)):
arr[i] += 1
print(arr[:])
if __name__ == "__main__":
arr = Array("i", [0, 0, 0]) # 'i' 表示整数类型
processes = [Process(target=increment, args=(arr,)) for _ in range(30)]
for p in processes:
p.start()
for p in processes:
p.join()
5. 进程间同步
1. 互斥锁——Lock
from multiprocessing import Lock, Process, Value
def increment(num, lock):
lock.acquire()
num.value += 1
print(num.value)
lock.release()
# 使用with可以简化,自动获取和释放锁
# def increment(num, lock):
# with lock:
# num.value += 1
# print(num.value)
if __name__ == "__main__":
lock = Lock()
counter = Value("i", 0) # 'i' 表示整数类型
processes = [Process(target=increment, args=(counter, lock)) for _ in range(30)]
for p in processes:
p.start()
for p in processes:
p.join()
2. 信号量
from multiprocessing import Process, Semaphore, Value
def increment(num, sem):
# 1. 尝试获取信号量(拿钥匙),如果没有钥匙了,进程会在这里阻塞等待
sem.acquire()
try:
# 2. 核心临界区:此时只有一个进程能进入这里,安全地修改数据
num.value += 1
print(num.value)
finally:
# 3. 无论如何,执行完后必须释放信号量(还钥匙),否则后面的进程会永远死锁
sem.release()
if __name__ == "__main__":
counter = Value("i", 0) # 共享变量
# 创建一个容量为 1 的信号量(相当于互斥锁,同一时间只允许 1 个进程访问)
sem = Semaphore(1)
# 将信号量 sem 作为参数传给子进程
processes = [Process(target=increment, args=(counter, sem)) for _ in range(30)]
for p in processes:
p.start()
for p in processes:
p.join()
print(f"最终结果: {counter.value}") # 必定雷打不动输出 30

==总结==:



6. IPC-Manager对象
from multiprocessing import Manager, Process
def update_dict(shared_dict):
shared_dict["common"] = shared_dict["common"] + 1
print(shared_dict)
if __name__ == "__main__":
with Manager() as manager:
shared_dict = manager.dict()
shared_dict["common"] = 0
processes = [
Process(target=update_dict, args=(shared_dict,)) for _ in range(30)
]
for p in processes:
p.start()
for p in processes:
p.join()

7. IPC-Pipe管道
TODO:linux开发中,c语言调用linux系统api创建的管道怎么做的
from multiprocessing import Pipe
conn1, conn2 = Pipe()
conn1.send("Hello")
print(conn2.recv())
conn2.send("World")
print(conn1.recv())


8. IPC-队列
队列的特点是是先进先出FIFO,multiprocessing.Queue是Python中线程安全的阻塞队列,能自动避免数据竞争问题(消息队列)
import queue
from multiprocessing import Queue
q = Queue(3)
q.put("1")
q.put("2")
q.put("3")
# q.put(3) # 队列已满,put被阻塞
try:
q.put_nowait("4") # 非阻塞方式:但会抛出异常
except queue.Full:
print("队列已经满了")
print(q.full())
print(q.get())
print(q.get())
print(q.get())
print(q.get()) # 队列已空,get被阻塞
生成这消费者模型,生产环境手动使用这些api很少,一般直接使用中间件,消息队列,比如RabbitMQ和Kafka.这些中间件提供了消息不丢失,持久化等开箱即用功能。
在linux操作系统中,进程间通信有哪些方式?



==总结==:


9. 进程池
1. 同步执行
因为阻塞知道任务完成,所以多个任务串行,适合需要保证顺序的场景
from multiprocessing import Pool
import time
def worker(n):
"""模拟耗时任务"""
time.sleep(n)
return f"任务 {n} 完成"
if __name__ == '__main__':
with Pool(4) as pool:
print("开始执行...")
# 同步执行,会阻塞直到完成
result = pool.apply(worker, (2,))
print(f"结果: {result}") # 2秒后才输出
2. 异步执行
apply_async立即返回,任务在后台执行,不阻塞主进程
from multiprocessing import Pool
import time
def worker(n):
time.sleep(n)
return f"任务 {n}"
if __name__ == '__main__':
with Pool(4) as pool:
# 提交多个异步任务
results = []
for i in range(5, 0, -1):
result = pool.apply_async(worker, (i,))
results.append(result)
print(f"已提交任务 {i}")
# 收集所有结果
for i, result in enumerate(results):
print(f"获取结果 {i+1}: {result.get()}")
异步的回调写法
import time
from multiprocessing import Pool
def worker(n):
time.sleep(n)
return n * 2
def callback(result):
"""成功时的回调函数"""
print(f"任务完成,结果: {result}")
def error_callback(error):
"""失败时的回调函数"""
print(f"任务失败: {error}")
if __name__ == "__main__":
with Pool(4) as pool:
# 添加回调函数
result = pool.apply_async(
worker, args=(2,), callback=callback, error_callback=error_callback
)
# 主进程可以继续做其他事
print("主进程继续执行...")
# 确保所有任务完成
result.wait()


python多线程
全局解释器锁GIL
英文全称Global Interpreter Lock。CPython(官方 Python)的解释器内部有一把全局锁,==同一时刻只允许一个线程执行 Python 字节码==。它的存在是为了保护CPython的内存管理(引用计数)不出现竞态条件,是一个历史设计决策。两个线程在做纯计算时(cpu密集型任务)都需要持有 GIL 才能跑。它们只能轮流持有锁、交替运行,永远无法真正并行。更糟的是,线程切换本身还有开销,实际跑下来甚至比单线程还慢。
线程创建和多线程运行
import threading
from time import sleep
def func(name):
print(f"以子线程方式运行{name}")
sleep(10)
if __name__ == "__main__":
t = threading.Thread(target=func, args=("test",)) # 创建线程对象
t.start() # 启动线程
# t.join() # 可选:等待子线程结束
print("主线程运行结束")


数据竞争问题,因为线程可以共享进程的全局变量,因此可以直接通过全局变量进行数据传递,以下模拟一个数据竞争问题
import time
from threading import Thread
counter = 0
def increment():
global counter
for _ in range(1000): # 每个线程1000次
tmp = counter
time.sleep(0.000001) # 关键:在读和写之间切换
counter = tmp + 1
threads = [Thread(target=increment) for _ in range(100)] # 总共10万次
for t in threads:
t.start()
for t in threads:
t.join()
print(f"期望: 100000, 实际: {counter}, 丢失: {100000 - counter}")
使用锁限制读写,确保同一时间只有一个线程操作
import time
from threading import Lock, Thread
lock = Lock()
counter = 0
def increment():
global counter
with lock:
for _ in range(1000): # 每个线程1000次
tmp = counter
time.sleep(0.000001) # 关键:在读和写之间切换
counter = tmp + 1
threads = [Thread(target=increment) for _ in range(100)] # 总共10万次
for t in threads:
t.start()
for t in threads:
t.join()
print(f"期望: 100000, 实际: {counter}, 丢失: {100000 - counter}")

线程同步
线程同步方案与进程类似,知道有数据竞争问题是核心,与进程一样,使用锁、信号量可以解决。条件变量和事件不做要求。知道存在即可。
线程共享方案
1. 共享全局变量+Lock
2. 队列
import queue
import threading
q = queue.Queue(1)
def worker():
data = q.get()
print(f"收到: {data}")
threading.Thread(target=worker).start()
q.put("hello")
q.put("world")
q.put("!")
跟随线程
Thread(target=increment, daemon=True)
线程池
from concurrent.futures import ThreadPoolExecutor
def square(n):
return n * n
# 使用with不用自己手动关闭线程池
# executor = ThreadPoolExecutor(max_workers=3)
# executor.shutdown(wait=True)
with ThreadPoolExecutor(max_workers=3) as executor:
resultMap = list(executor.map(square, [1, 2, 3, 4, 5]))
print(resultMap)
resultSinge = []
for task in [1, 2, 3, 4, 5]:
r = executor.submit(square, task)
resultSinge.append(r.result())
print(resultSinge)
import time
from concurrent.futures import ThreadPoolExecutor
import requests
urls = [
"https://www.baidu.com",
"https://www.zhihu.com",
"https://www.bilibili.com",
"https://www.baidu.com",
"https://www.zhihu.com",
"https://www.bilibili.com",
"https://www.baidu.com",
"https://www.zhihu.com",
"https://www.bilibili.com",
]
def fetch(url):
print(f"开始下载: {url}")
resp = requests.get(url, timeout=5, verify=False)
print(f"完成下载: {url}, 大小={len(resp.content)}")
return len(resp.content)
# ========== 多线程版本(使用 submit + shutdown)==========
print("=== 多线程版本 ===")
start = time.time()
# 创建线程池
executor = ThreadPoolExecutor(max_workers=5)
# 提交所有任务,保存 future 对象
futures = []
for url in urls:
future = executor.submit(fetch, url)
futures.append(future)
# 关闭线程池(不再接受新任务)
executor.shutdown(wait=True) # wait=True 等待所有任务完成
# 获取结果
results = [future.result() for future in futures]
thread_time = time.time() - start
print(f"多线程耗时: {thread_time:.2f}秒n")
# ========== 单线程版本 ==========
print("=== 单线程版本 ===")
start = time.time()
results_single = [fetch(url) for url in urls]
single_time = time.time() - start
print(f"单线程耗时: {single_time:.2f}秒n")
# ========== 对比 ==========
print("=== 对比 ===")
print(f"多线程: {thread_time:.2f}秒")
print(f"单线程: {single_time:.2f}秒")
print(f"提升倍数: {single_time / thread_time:.1f}x")
==总结==:

进程和线程的设计还是有区别的


python协程
单线程内做到并发执行。一个协程写法如同函数,只是执行的方式与普通函数不同
背景
假设我们要开发一个高并发的Web服务器,需要同时处理 10,000 个并发请求(C10K问题)。如果我们用“一台服务器 + 传统多线程(1个请求=1个线程)”的模型,会发生什么?
传统多线程的“三座大山”
内存撑爆: 在 64 位 Linux 系统中,一个线程的默认栈大小通常是 8MB(即便调小,最低一般也需要几十到几百KB)。10000个线程需要80G左右内存,硬件成本极高。
上下文切换(Context Switch)内耗: 线程是由操作系统内核调度的。当 10,000 个线程频繁切换时,操作系统光是保存和恢复 CPU 寄存器等,就会把 CPU 的算力消耗殆尽。CPU 都在忙着“办手续”,没空“干正事”。
线程竞争: 为了防止数据死锁或错乱,必须加锁(Lock),这会导致线程挂起,进一步加剧切换开销。
能不能有一种技术,既能享受异步I/O的高效,又能像写同步代码一样简单,而且轻量到一台机器能跑几十万个? 这就是协程(Coroutine)。
对比普通函数(1分钟)
普通函数:调用 → 执行 → return(结束)
协程函数:调用 → 执行 → await(暂停)→ 恢复 → 继续
核心语法
async def:定义协程函数
await:启动并运行协程函数,且等待直到拿到协程函数的返回值
asyncio.run():启动事件循环
import asyncio
async def asyncTask():
print("hello world")
return 1
async def main():
a = asyncTask() # 异步任务此时并没有执行,只是拿到了一个协程对象
#a = create_task(asyncTask())
result = await a # 异步任务在此启动运行,并直到运行结束,把协程函数返回值赋值给result
print(result)
asyncio.run(main())
==重点知识==
1. await必须在async函数中
2. await可以启动并拿到一个async函数的执行结果
并发的关键操作
一定要让任务立即提交给队列,才能做到并发。
而常用的gather和create_task都可以做到
使用gather同时运行多个任务
多个任务同时执行,同时拿到所有任务的结果。如果任何一个任务失败,抛出异常;如果使用了asyncio.gather(task_a, task_b, return_exceptions=True)此参数,不会整体抛出异常,仅失败的抛出。
import asyncio
import time
start = time.time()
async def task(name, delay):
await asyncio.sleep(delay)
print(f"{name} 完成")
return name
# async def taskErr():
# raise RuntimeError("任务出错")
async def main():
# 创建3个任务,分别延迟2秒、1秒、3秒
# await task("A", 2)
# await task("B", 1)
# await task("c", 3)
results = await asyncio.gather(task("A", 2), task("B", 1), task("C", 3))
print(results)
asyncio.run(main())
end = time.time()
print(f"耗时: {end - start:.2f}秒")

怎么识别异步任务?
1. async函数的能力范围
async函数只是声明了一个协程函数,并不是当中的操作都自动变成异步了。如果你依然使用了内置的同步api,那事件循环在执行async函数时,依然是阻塞状态。如下示例
import asyncio
import time
from asyncio.tasks import create_task
async def asyncTask():
time.sleep(1) # 你觉得会被放入队列的代码,所有的操作都需提供异步支持
async def main():
start = time.time()
a = create_task(asyncTask())
b = create_task(asyncTask())
c = create_task(asyncTask())
await a
await b
await c
end = time.time()
print(f"Total took {end - start} seconds")
asyncio.run(main())
2. 如何识别异步函数呢
一般一个库函数支持异步,会在文档中声明支持async/await
常用的功能异步库
网络请求: httpx
import asyncio
import time
import httpx
async def task():
client = httpx.AsyncClient()
start = time.time()
response = await client.get("https://www.baidu.com")
end = time.time()
print(f"一次请求所花时间{end - start}秒")
print(response.status_code)
await client.aclose()
async def main():
tasks = []
for _ in range(15):
tasks.append(asyncio.create_task(task()))
for t in tasks:
await t
mainStart = time.time()
asyncio.run(main())
mainEnd = time.time()
print(f"总耗时{mainEnd - mainStart}秒")
文件读写: aiofiles
那如果实在要在写成函数中使用同步api怎么办呢?使用run_in_executor包裹一层
import asyncio
import time
from asyncio.tasks import create_task
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor()
async def asyncTask():
loop = asyncio.get_event_loop()
# 线程池封装,其实削足适履了,就用不了协程特性了
await loop.run_in_executor(executor, time.sleep, 1)
async def main():
start = time.time()
a = create_task(asyncTask())
b = create_task(asyncTask())
c = create_task(asyncTask())
await a
await b
await c
end = time.time()
print(f"Total took {end - start} seconds")
asyncio.run(main())


==总结==:




==本质总结==:


协程同步
见课件
课程结尾
1. 希望大家自己学习git,配合国内的gitee.com学会代码管理和分支管理
==全部总结==:
