你是不是也遇到过这样的场景:写了个爬虫脚本,明明网络带宽足够,但抓取1000个页面却要等上半小时;或者开发了一个Web服务,用户稍微多点就响应缓慢,CPU却闲得发慌;又或者处理一批数据文件,只能眼睁睁看着程序单线程“吭哧吭哧”跑,而其他核心在“围观”。
问题根源往往不在于算法,而在于并发。很多Python开发者,尤其是从脚本入门的朋友,对“并发编程”存在一种微妙的距离感:知道它重要,面试常考,但总觉得概念复杂(线程、进程、协程、锁、队列……),写起来又容易出Bug(数据竞争、死锁、GIL限制),于是宁愿用time.sleep或者粗暴的循环,也不敢轻易踏入这个领域。
这种“不敢用”和“用不好”,正在成为Python进阶路上最大的效率瓶颈。尤其在2026年的今天,无论是数据密集型应用、高并发微服务,还是AI推理流水线,对并发能力的要求都已是标配。好消息是,Python的并发生态已经非常成熟,从基础的threading、multiprocessing到高效的asyncio,再到concurrent.futures这样的高层抽象,工具链清晰完整。
本文将为你彻底拆解Python并发编程。我们不空谈理论,而是聚焦于一个核心判断:Python并发编程的难点,不在于API调用,而在于对“并发模型”和“同步原语”的精准选择与正确使用。你将通过一套从零基础到实战的完整路径,理解多线程、多进程、线程同步、进程通信等核心概念,掌握ThreadLocal等高级技巧,并最终能设计出稳定、高效的并发程序。文章包含大量可直接运行的代码示例、常见陷阱分析和生产环境最佳实践,目标是让你读完就能用,用了不出错。
1. 为什么2026年了,你还需要深入学习Python并发?
在AI工具和低代码平台日益流行的今天,有人可能会问:并发这种“底层”知识还有必要学吗?答案是:更有必要了。原因有三:
第一,硬件趋势倒逼。多核CPU已是消费级电脑的标配,服务器动辄数十甚至上百核心。如果你的Python程序只能占用一个核心,就相当于开着八车道的高速公路却只用一个车道,是对计算资源的巨大浪费。并发编程是释放硬件潜力的钥匙。
第二,问题域的本质要求。现代应用本质上是并发的:Web服务器要同时处理成千上万个请求;数据处理管道需要并行清洗、转换多个数据源;GUI应用要在后台执行任务时不阻塞用户界面。这些场景,单线程模型根本无法胜任。
第三,Python自身的进化。许多人因为“GIL”(全局解释器锁)而对Python多线程嗤之以鼻,认为其无法实现真正的并行计算。这其实是一个经典的误解。GIL确实限制了CPU密集型任务中多线程的并行能力,但对于I/O密集型任务(网络请求、文件读写、数据库查询),多线程能极大提升吞吐量,因为线程在等待I/O时会让出GIL。而对于CPU密集型任务,Python提供了multiprocessing模块来绕过GIL,实现真正的多进程并行。此外,asyncio提供的异步I/O模型,在超高并发连接场景下性能卓越。2026年的Python并发工具箱,早已不是一把锤子,而是一整套适应不同场景的精密工具。
因此,学习Python并发,不是选择学不学,而是必须掌握的核心技能。它能直接决定你开发的程序是“玩具”还是“生产级”应用。
2. 核心概念辨析:线程、进程、协程与GIL
在动手写代码前,必须厘清几个最容易混淆的核心概念。理解它们的区别和适用场景,是做出正确技术选型的前提。
| 概念 | 定义 | 内存空间 | 切换开销 | Python中受GIL影响 | 典型应用场景 |
|---|---|---|---|---|---|
| 进程 (Process) | 操作系统资源分配的基本单位,有独立的内存空间。 | 独立 | 大 | 否 (每个进程有独立GIL) | CPU密集型计算、需要强隔离的任务 |
| 线程 (Thread) | 进程内的执行单元,共享进程的内存空间。 | 共享 | 小 | 是 | I/O密集型任务、需要共享状态的并发操作 |
| 协程 (Coroutine) | 用户态轻量级线程,由程序控制调度,而非操作系统。 | 共享 | 极小 | 是 (但通过异步规避阻塞) | 超高并发网络I/O (如万级连接) |
关于GIL的深入理解:GIL是CPython解释器(即我们通常使用的Python)中的一个互斥锁,它确保同一时刻只有一个线程执行Python字节码。这简化了CPython的内存管理(如垃圾回收),但也带来了限制。关键在于:GIL只影响CPU密集型任务。当线程执行I/O操作(如socket.recv()、file.read())或调用某些C扩展库(如numpy、zlib)时,会主动释放GIL。因此,对于网络爬虫、Web服务后端等I/O等待时间长的任务,多线程依然能显著提升性能。
一个简单的类比:
- 进程就像一家独立的公司,有自己独立的办公楼(内存)、财务(资源)。公司间通信(进程通信)需要走正式的商务流程(IPC),成本高但隔离性好。
- 线程就像一家公司里的不同部门,共享办公楼和基础设施(内存)。部门间协作(线程通信)非常方便,但需要管理好公共资源的使用权(同步),否则会乱套。
- 协程更像公司里一个特别高效的团队,团队成员(协程)在同一个工位上快速交接工作,一个人等待时立刻换另一个人上,极致利用时间,但要求所有工作都是可协作的(异步非阻塞)。
3. 环境准备与前置说明
本文所有代码示例基于Python 3.8+版本,因为asyncio的API在3.7之后才趋于稳定,且concurrent.futures等模块功能完善。大部分代码在Windows、macOS和Linux上均可运行,但进程相关示例在Windows上创建方式略有不同(spawn启动方式),本文会做说明。
开发环境建议:
- Python解释器:使用官方CPython 3.8, 3.9, 3.10或3.11。可通过
python --version检查。 - IDE:任何你熟悉的即可,如PyCharm、VS Code(安装Python插件)、Jupyter Notebook(适合演示)。
- 关键模块:
threading,multiprocessing,concurrent.futures,asyncio,queue均为Python标准库,无需额外安装。
运行示例的通用步骤:
- 将代码保存为
.py文件。 - 在终端或IDE中运行该文件:
python your_script.py。 - 观察输出,理解并发执行与顺序执行的差异。
4. 多线程编程实战:从threading到ThreadPoolExecutor
让我们从最常用的threading模块开始。创建线程有两种主要方式:继承Thread类和传入可调用对象。
4.1 基础线程创建与管理
示例1:通过函数创建线程
import threading import time def task(name, delay): """一个简单的任务,打印信息并睡眠""" print(f"线程 {name} 开始运行,休眠 {delay} 秒") time.sleep(delay) print(f"线程 {name} 运行结束") if __name__ == "__main__": print("主线程开始") # 创建线程对象,target指定要运行的函数,args传入参数 t1 = threading.Thread(target=task, args=("A", 2)) t2 = threading.Thread(target=task, args=("B", 1)) # 启动线程 t1.start() t2.start() print("主线程在等待子线程...") # 等待线程结束 t1.join() t2.join() print("所有线程执行完毕,主线程结束")运行与观察:执行上述代码,你会看到类似以下的输出,注意“主线程在等待子线程...”这句话打印的时机,以及线程A和B结束的顺序(B先结束),这直观展示了并发执行的非确定性。
主线程开始 线程 A 开始运行,休眠 2 秒 线程 B 开始运行,休眠 1 秒 主线程在等待子线程... 线程 B 运行结束 线程 A 运行结束 所有线程执行完毕,主线程结束4.2 线程同步:锁(Lock)解决数据竞争
当多个线程需要修改同一个共享资源(如全局变量、列表、字典)时,就会发生数据竞争,导致结果不可预期。锁(Lock)是最基本的同步原语,用于确保同一时间只有一个线程能进入“临界区”代码。
示例2:没有锁的灾难(错误示范)
import threading counter = 0 # 共享资源 def increment(): global counter for _ in range(100000): # 每个线程增加10万次 # 这三步操作不是原子的:读取 -> 修改 -> 写回 temp = counter temp = temp + 1 counter = temp if __name__ == "__main__": threads = [] for i in range(5): # 创建5个线程 t = threading.Thread(target=increment) threads.append(t) t.start() for t in threads: t.join() print(f"理论结果: 500000, 实际结果: {counter}")多次运行此代码,你会发现counter的结果几乎每次都小于500000,这就是数据竞争导致的丢失更新。
示例3:使用锁保护共享资源
import threading counter = 0 lock = threading.Lock() # 创建一把锁 def increment_with_lock(): global counter for _ in range(100000): lock.acquire() # 获取锁,进入临界区 try: # 临界区内的操作是安全的 temp = counter temp = temp + 1 counter = temp finally: lock.release() # 释放锁,一定要在finally中确保释放 if __name__ == "__main__": threads = [] for i in range(5): t = threading.Thread(target=increment_with_lock) threads.append(t) t.start() for t in threads: t.join() print(f"理论结果: 500000, 实际结果: {counter} (使用锁)")现在,无论运行多少次,结果都是正确的500000。关键点:锁的使用必须非常小心,获取锁后一定要释放(通常用try...finally块或with语句),否则会导致死锁。
更优雅的写法:使用with语句
def increment_with_lock_elegant(): global counter for _ in range(100000): with lock: # with 语句自动获取和释放锁 counter += 1 # 这个操作现在是原子的4.3 线程池:更高效的管理方式
频繁创建和销毁线程开销很大。concurrent.futures模块提供的ThreadPoolExecutor是一个高级接口,它管理着一个线程池,可以提交任务并异步获取结果。
示例4:使用ThreadPoolExecutor执行I/O密集型任务假设我们需要下载多个网页内容。
import concurrent.futures import requests import time def download_url(url): """模拟下载网页内容(I/O操作)""" try: # 这里使用requests库,需要先安装: pip install requests response = requests.get(url, timeout=5) return f"{url}: 状态码 {response.status_code}, 长度 {len(response.text)}" except Exception as e: return f"{url}: 错误 - {e}" if __name__ == "__main__": urls = [ "https://httpbin.org/delay/1", # 模拟延迟1秒的接口 "https://httpbin.org/delay/2", "https://httpbin.org/status/200", "https://httpbin.org/status/404", "https://www.example.com", ] print("=== 顺序执行(慢)===") start = time.time() for url in urls: print(download_url(url)) print(f"顺序执行耗时: {time.time() - start:.2f} 秒\n") print("=== 使用线程池并发执行(快)===") start = time.time() # 使用 with 语句管理线程池,max_workers 指定最大线程数 with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: # 提交所有任务,map会保持输入顺序 results = executor.map(download_url, urls) for result in results: print(result) print(f"线程池执行耗时: {time.time() - start:.2f} 秒")你会看到,并发执行的耗时远小于各任务延迟的累加,因为线程在等待网络响应时,其他线程可以继续工作。这就是多线程解决I/O瓶颈的威力。
5. 多进程编程实战:突破GIL,实现真并行
对于CPU密集型任务(如计算圆周率、图像处理、科学计算),多线程由于GIL无法利用多核。这时就需要使用multiprocessing模块,它通过创建独立的Python解释器进程来实现真正的并行计算。
5.1 创建进程与进程池
示例5:基础多进程计算
import multiprocessing import time def cpu_bound_task(n): """模拟一个CPU密集型计算:计算n的平方和""" result = sum(i * i for i in range(n)) return result if __name__ == '__main__': # 多进程编程必须有的保护 numbers = [5_000_000, 6_000_000, 7_000_000, 8_000_000] print("=== 顺序执行 ===") start = time.time() for n in numbers: cpu_bound_task(n) sequential_time = time.time() - start print(f"顺序执行耗时: {sequential_time:.2f} 秒\n") print("=== 多进程并行执行 ===") start = time.time() with multiprocessing.Pool(processes=4) as pool: # 创建包含4个进程的池 # 使用 map 方法将任务分发给进程池 pool.map(cpu_bound_task, numbers) parallel_time = time.time() - start print(f"多进程执行耗时: {parallel_time:.2f} 秒") print(f"加速比: {sequential_time / parallel_time:.2f}x")运行这个例子,你会看到多进程版本的速度接近顺序版本的4倍(在4核CPU上),实现了真正的并行加速。
5.2 进程间通信(IPC)
进程拥有独立内存空间,不能像线程那样直接共享变量。multiprocessing模块提供了多种进程间通信(IPC)机制,如Queue、Pipe和共享内存(Value,Array)。
示例6:使用Queue进行进程间通信Queue是线程和进程安全的队列,常用于生产者-消费者模型。
import multiprocessing import time import random def producer(queue, name): """生产者进程:生成数据放入队列""" for i in range(5): item = f"{name}-产品{i}" time.sleep(random.uniform(0.1, 0.5)) # 模拟生产时间 queue.put(item) print(f"[生产者 {name}] 生产了 {item}") # 放入结束信号 queue.put(None) def consumer(queue, name): """消费者进程:从队列取出数据并处理""" while True: item = queue.get() if item is None: # 收到结束信号 queue.put(None) # 为其他消费者传递信号 print(f"[消费者 {name}] 收到结束信号,退出") break time.sleep(random.uniform(0.2, 0.8)) # 模拟处理时间 print(f"[消费者 {name}] 处理了 {item}") if __name__ == '__main__': # 创建进程间通信的队列 queue = multiprocessing.Queue(maxsize=3) # 设置队列最大容量 # 创建进程 producers = [ multiprocessing.Process(target=producer, args=(queue, f"P{i}")) for i in range(2) ] consumers = [ multiprocessing.Process(target=consumer, args=(queue, f"C{i}")) for i in range(3) ] # 启动所有进程 for p in producers + consumers: p.start() # 等待生产者结束 for p in producers: p.join() # 等待消费者结束 for c in consumers: c.join() print("所有进程执行完毕")6. 高级主题:ThreadLocal与异步编程asyncio初探
6.1 ThreadLocal:线程的私有储物柜
在多线程环境中,有时你需要一些变量是线程私有的,每个线程都有自己的副本,互不干扰。全局变量和传递参数都不够优雅,这时threading.local()就派上用场了。
示例7:使用ThreadLocal管理数据库连接在Web应用中,每个请求在一个独立线程中处理,通常需要独立的数据库连接。
import threading import sqlite3 # 以sqlite为例,实际可能是pymysql, psycopg2等 import time # 创建一个ThreadLocal实例 local_data = threading.local() def get_db_connection(): """获取当前线程的数据库连接,如果没有则创建""" # 检查当前线程是否有‘conn’属性 if not hasattr(local_data, 'conn'): print(f"{threading.current_thread().name}: 创建新的数据库连接") # 模拟创建连接(实际这里会包含主机、端口、用户名密码等) local_data.conn = f"Connection-{threading.current_thread().name}" return local_data.conn def close_db_connection(): """关闭当前线程的数据库连接""" if hasattr(local_data, 'conn'): print(f"{threading.current_thread().name}: 关闭数据库连接") del local_data.conn def process_request(request_id): """模拟处理一个HTTP请求""" conn = get_db_connection() print(f"线程 {threading.current_thread().name} 处理请求 {request_id}, 使用连接 {conn}") time.sleep(0.1) # 模拟数据库操作 # 请求处理完毕,通常不会立即关闭连接,可能放回连接池 # 这里为了演示,在线程结束时关闭 # close_db_connection() # 实际可能由框架或中间件管理 if __name__ == "__main__": threads = [] for i in range(5): t = threading.Thread(target=process_request, args=(i,), name=f"Thread-{i}") threads.append(t) t.start() for t in threads: t.join() print("所有请求处理完毕")输出会显示,每个线程都创建并使用了自己独立的“连接”对象,实现了线程间的资源隔离。
6.2 asyncio异步编程简介
asyncio是Python用于编写并发代码的库,使用async/await语法。它特别适合处理大量I/O操作且连接生命周期长的场景(如聊天服务器、实时数据推送)。与多线程不同,asyncio是单线程的,通过事件循环在多个任务间快速切换,在I/O等待时执行其他任务。
示例8:一个简单的asyncio示例
import asyncio import time async def say_after(delay, what): """一个异步任务,等待一段时间后打印信息""" await asyncio.sleep(delay) # 异步等待,让出控制权 print(what) return f"{what} done at {time.time()}" async def main(): print(f"程序开始于 {time.time()}") # 并发运行两个异步任务 task1 = asyncio.create_task(say_after(1, 'Hello')) task2 = asyncio.create_task(say_after(2, 'World')) # 等待两个任务完成并获取结果 result1 = await task1 result2 = await task2 print(f"任务1结果: {result1}") print(f"任务2结果: {result2}") print(f"程序结束于 {time.time()}") # Python 3.7+ if __name__ == "__main__": asyncio.run(main())运行这个程序,你会发现总耗时大约是2秒,而不是1+2=3秒,因为两个asyncio.sleep是并发等待的。这对于处理成千上万个并发的网络连接至关重要。
7. 并发实战:构建一个简单的Web爬虫
让我们综合运用线程池和队列,构建一个更实用的、可控的并发爬虫。
示例9:带并发控制和错误处理的爬虫
import concurrent.futures import requests from urllib.parse import urljoin import threading from queue import Queue import time class ConcurrentCrawler: def __init__(self, start_url, max_workers=5, max_pages=20): self.start_url = start_url self.max_workers = max_workers self.max_pages = max_pages self.visited = set() self.lock = threading.Lock() self.page_count = 0 self.to_crawl = Queue() self.to_crawl.put(start_url) def fetch(self, url): """获取页面内容,模拟解析链接""" try: # 在实际爬虫中,这里需要添加headers、代理、重试逻辑等 response = requests.get(url, timeout=3) response.raise_for_status() # 简单模拟解析出一些链接(实际应用需要用BeautifulSoup等) # 这里我们只是返回一个假想的链接列表 fake_links = [urljoin(url, f"page{i}") for i in range(2)] return response.text[:100], fake_links # 返回前100字符和假链接 except Exception as e: print(f"抓取 {url} 失败: {e}") return None, [] def worker(self): """工作线程函数""" while self.page_count < self.max_pages: try: url = self.to_crawl.get(timeout=2) except: break # 队列为空超时,退出 # 检查是否已访问 with self.lock: if url in self.visited: self.to_crawl.task_done() continue self.visited.add(url) self.page_count += 1 if self.page_count > self.max_pages: break print(f"正在抓取 ({self.page_count}/{self.max_pages}): {url}") content, links = self.fetch(url) if content: print(f" 成功: 获取到 {len(content)} 字符") # 将新发现的链接加入队列 for link in links: if link not in self.visited: self.to_crawl.put(link) else: print(f" 失败") self.to_crawl.task_done() time.sleep(0.5) # 礼貌延迟,避免对目标服务器造成压力 def run(self): """启动爬虫""" print(f"开始并发爬虫,起始URL: {self.start_url}") start_time = time.time() with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: # 提交工作线程任务 futures = [executor.submit(self.worker) for _ in range(self.max_workers)] # 等待队列中所有任务被处理 self.to_crawl.join() # 通知工作线程退出 for _ in range(self.max_workers): self.to_crawl.put(None) # 等待所有工作线程结束 concurrent.futures.wait(futures) elapsed = time.time() - start_time print(f"\n爬虫结束。共抓取 {len(self.visited)} 个页面,耗时 {elapsed:.2f} 秒") if __name__ == "__main__": # 注意:请替换为一个你可以合法抓取的测试URL,或使用httpbin等测试站点 crawler = ConcurrentCrawler(start_url="https://httpbin.org/html", max_workers=3, max_pages=10) crawler.run()这个爬虫示例包含了并发控制(线程池)、任务队列(Queue)、共享状态同步(Lock)、错误处理和礼貌爬取延迟,是一个接近实际应用的小型项目框架。
8. 常见问题与排查思路
并发编程的Bug往往难以复现和调试。下表列出了一些典型问题及解决方法。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 程序偶尔结果错误,且每次错误值不同 | 数据竞争:多个线程/进程同时读写共享变量未加锁。 | 检查所有共享变量(全局变量、类属性、传递的引用类型对象)的修改处。使用threading.Lock或multiprocessing.Lock。 | 使用锁(Lock)、信号量(Semaphore)或使用线程安全的数据结构(queue.Queue)。 |
| 程序卡死,不再输出 | 死锁:两个及以上线程/进程互相等待对方释放锁。 | 检查锁的获取顺序。是否在持有锁A时去申请锁B,而另一个线程正持有锁B申请锁A? | 1. 统一锁的获取顺序。 2. 使用 with语句管理锁,避免忘记释放。3. 使用 threading.RLock(可重入锁)或设置超时lock.acquire(timeout=5)。 |
| 多线程程序CPU使用率没有提升,甚至更慢 | 1. 任务是CPU密集型,受GIL限制。 2. 锁竞争过于激烈,线程大部分时间在等待。 3. 创建/销毁线程开销过大。 | 1. 用cProfile分析程序热点。2. 检查锁的粒度是否过粗。 | 1. CPU密集型任务改用multiprocessing。2. 减小锁的粒度或使用无锁数据结构。 3. 使用 ThreadPoolExecutor复用线程。 |
| 多进程程序报错“PicklingError” | 传递给进程函数的参数或返回值不能被序列化(pickle)。 | 检查进程函数参数和返回值中的对象是否支持pickle。自定义类需要实现__getstate__和__setstate__方法。 | 1. 只传递基本类型、列表、字典等可序列化对象。 2. 使用 multiprocessing.Manager创建共享对象。3. 将复杂数据存入数据库或文件进行共享。 |
asyncio程序报错“RuntimeError: Event loop is closed” | 在Windows系统上,事件循环策略问题或未正确关闭。 | 确保使用asyncio.run(main())(Python 3.7+)来运行顶级入口函数。 | 1. 使用asyncio.run()。2. 如果必须手动管理,确保在finally块中关闭循环: loop.close()。 |
| 线程/进程数量过多导致系统资源耗尽 | 未限制并发数,例如在循环中无限创建线程。 | 监控系统内存和线程/进程数。 | 使用线程池(ThreadPoolExecutor)或进程池(multiprocessing.Pool)来限制最大并发数。 |
9. 最佳实践与工程建议
掌握了基础之后,要将并发代码用于生产环境,还需要遵循以下最佳实践:
明确任务类型,正确选择模型
- I/O密集型(网络、磁盘、数据库):优先考虑多线程或
asyncio。如果连接数极高(>1000),asyncio通常更有优势。 - CPU密集型(计算、数据处理):必须使用多进程(
multiprocessing)来利用多核。 - 混合型:可以考虑“多进程+多线程/协程”的混合模型,例如用多进程处理CPU密集型部分,每个进程内用多线程处理I/O。
- I/O密集型(网络、磁盘、数据库):优先考虑多线程或
使用高层抽象,避免直接操作底层原语
- 尽量使用
concurrent.futures.ThreadPoolExecutor和ProcessPoolExecutor,而不是手动管理threading.Thread或multiprocessing.Process。池化管理更安全、高效。 - 使用
queue.Queue或multiprocessing.Queue进行线程/进程间通信,它们是线程/进程安全的。
- 尽量使用
锁的粒度要细,持有时间要短
- 只锁住真正需要保护的共享数据操作,锁住的范围(临界区)越小越好。
- 绝对避免在持有锁的情况下进行I/O操作(如网络请求、文件读写),这会导致性能急剧下降。
善用
ThreadLocal和上下文变量- 对于像数据库连接、请求上下文这类需要线程隔离的数据,使用
threading.local()或contextvars(Python 3.7+)是优雅的选择。
- 对于像数据库连接、请求上下文这类需要线程隔离的数据,使用
做好异常处理
- 并发任务中的异常不会自动传播到主线程。使用
concurrent.futures时,通过future.result()获取结果时会抛出异常,或者用future.exception()检查。 - 在线程/进程函数内部用
try...except捕获并记录异常,避免静默失败。
- 并发任务中的异常不会自动传播到主线程。使用
设置超时和资源限制
- 对网络请求、锁获取、队列获取等操作设置超时(
timeout参数),防止程序永久挂起。 - 使用信号量(
Semaphore)或连接池来限制对稀缺资源(如数据库连接)的并发访问数。
- 对网络请求、锁获取、队列获取等操作设置超时(
编写可测试的并发代码
- 将并发逻辑与非并发逻辑(业务计算)分离,便于单元测试。
- 使用依赖注入,在测试时可以用同步实现或Mock对象替换并发组件。
Python并发编程是一个庞大但结构清晰的领域。从理解GIL的本质开始,到熟练运用多线程处理I/O、多进程榨干CPU、以及用asyncio应对海量连接,每一步都对应着不同的应用场景和解决方案。记住,没有银弹,最有效的并发模式永远取决于你要解决的具体问题。建议你从文中的示例代码开始,亲手运行并修改它们,观察不同的参数和行为。当你遇到复杂场景时,再回头查阅threading、multiprocessing和asyncio的官方文档,那里有最权威和详细的说明。