在 Python 的标准库中,threading 模块(用于线程)和 requests 库(用于 Session 对象)是两个独立但经常协同工作的概念。 它们解决的是不同层面的问题。

- 线程:是并发编程的基础,它允许你的程序在单个进程内同时执行多个任务(代码片段),这对于处理 I/O 密集型任务(如网络请求、文件读写)尤其有用,因为当一个线程等待 I/O 时,CPU 可以切换到其他线程去执行任务,从而提高程序的效率和响应速度。
- Session:是HTTP 通信中的一个概念,它代表了一个持久化的连接,可以让你在多个请求之间共享某些数据,cookies、headers(身份认证信息等),使用
Session可以避免在每次请求时都重新建立 TCP 连接(即“握手”过程),从而显著提高性能。
下面,我们将分三部分来详细解释:
- Python 线程 (
threading模块) - Python Session (
requests.Session对象) - 线程与 Session 的协同工作
Python 线程 (threading 模块)
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
为什么使用线程?
- 提高响应速度:对于有用户界面的程序,可以使用一个线程来响应用户输入,另一个线程来执行耗时任务,防止界面卡死。
- 利用多核 CPU:对于 CPU 密集型任务,可以使用多线程(或多进程)来并行计算,充分利用多核处理器的性能。
- 简化 I/O 操作:对于网络请求、数据库查询等 I/O 密集型任务,使用多线程可以避免在等待 I/O 时阻塞整个程序。
threading 模块基本用法
threading 模块是 Python 的标准库,提供了丰富的线程管理功能。
核心概念:

Thread类:用于创建和管理线程。target:线程要执行的函数。args:传递给目标函数的位置参数(元组)。start():启动线程,此时线程进入“就绪”状态,等待 CPU 调度。join():主线程等待子线程执行完毕后再继续执行,这常用于确保所有子任务都完成后,主线程再进行下一步操作。
示例:
import threading
import time
def worker(num):
"""这是一个线程要执行的函数"""
print(f"Worker {num} is starting...")
time.sleep(2) # 模拟耗时操作,比如网络请求或文件读写
print(f"Worker {num} has finished.")
if __name__ == "__main__":
threads = []
# 创建并启动 5 个线程
for i in range(5):
# 创建线程对象
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
# 启动线程
t.start()
# 主线程等待所有子线程完成
for t in threads:
t.join()
print("All threads have finished. Main thread continues.")
输出结果(顺序可能略有不同):
Worker 0 is starting...
Worker 1 is starting!
Worker 2 is starting!
Worker 3 is starting!
Worker 4 is starting!
... (等待 2 秒) ...
Worker 0 has finished.
Worker 1 has finished.
Worker 2 has finished.
Worker 3 has finished.
Worker 4 has finished.
All threads have finished. Main thread continues.
可以看到,主线程启动了 5 个工作线程后,并没有立即退出,而是通过 join() 等待它们全部完成。
Python Session (requests.Session 对象)
在 Web 开发和爬虫领域,Session 是一个非常实用的概念,它不是一个 Python 内置对象,而是 requests 库提供的一个高级接口。

为什么使用 Session?
- 保持会话状态:这是最重要的功能,很多网站(如需要登录的网站)通过 Cookie 来识别用户身份,当你使用
Session对象发起第一个请求并登录后,Session会自动保存服务器返回的 Cookie,后续所有通过该Session发起的请求都会自动带上这个 Cookie,从而维持登录状态。 - 性能提升:
Session对象会保持一个底层的 TCP 连接(requests默认使用urllib3库,它实现了连接池),当你使用同一个Session发起多个请求到同一个域名时,可以复用这个连接,避免了每次请求都进行 TCP 三次握手和四次挥手,大大降低了延迟。
requests.Session 基本用法
核心概念:
requests.Session():创建一个Session对象。session.get()/session.post():通过Session对象发起请求,用法与requests.get()/requests.post()完全相同。- 自动处理 Cookie:
Session会自动处理和管理 Cookie。
示例:
import requests
# 1. 创建一个 Session 对象
session = requests.Session()
# 2. 发起一个登录请求 (假设登录接口会设置 Cookie)
# login_url = "https://example.com/api/login"
# login_data = {"username": "test", "password": "123456"}
# response = session.post(login_url, data=login_data)
# print("Login status:", response.status_code)
# 3. 发起一个需要登录才能访问的请求
# profile_url = "https://example.com/api/profile"
# response = session.get(profile_url)
# print("Profile page:", response.text)
# 为了演示,我们用一个简单的例子代替
# 第一次请求,服务器可能会返回一个 Set-Cookie 头
print("--- Making first request ---")
response1 = session.get("http://httpbin.org/cookies/set/sessionid/123456")
print("Response 1 Cookies:", response1.cookies.get_dict()) # 应该能看到 sessionid
# 第二次请求,Session 会自动带上第一次获取的 Cookie
print("\n--- Making second request ---")
response2 = session.get("http://httpbin.org/cookies")
print("Response 2 Cookies:", response2.json()) # 会看到 sessionid 被发送回来了
# 关闭 Session,释放连接池资源
session.close()
在这个例子中,我们访问 httpbin.org 这个测试网站,它提供了一个 /cookies/set 接口,可以让我们设置一个 Cookie,第二次请求 /cookies 时,Session 自动将第一次设置的 sessionid 发送了回去,证明了会话的保持。
线程与 Session 的协同工作
我们把这两个概念结合起来,一个常见的场景是:在多线程环境中,使用 Session 对象并发地爬取一个需要登录的网站。
这里有一个非常重要的陷阱需要了解:requests.Session 对象是线程不安全的。
陷阱:Session 的线程不安全性
如果你在多个线程中共享同一个 Session 对象,并发的读写操作可能会导致数据混乱,
- Cookie 被一个线程的请求覆盖,导致另一个线程的认证失败。
- 连接池的状态被破坏,导致请求失败或性能下降。
正确的协同工作方式
为了避免线程安全问题,最佳实践是每个线程都创建自己的 Session 对象。
为什么这是可行的?
- 隔离性:每个线程有自己的
Session,就有自己的 Cookie 存储和连接池,一个线程的请求不会影响另一个线程。 - 资源共享:虽然
Session对象本身不共享,但登录所需的用户名、密码等共享数据可以作为参数传递给每个线程的函数。 - 资源效率:虽然每个线程都有自己的连接池,但对于现代操作系统来说,为每个线程维护一个小的连接池开销并不大,远比处理因共享
Session导致的混乱要好。
示例:多线程爬取需要登录的网站
import threading
import time
import requests
# 共享的登录凭证
USERNAME = "test_user"
PASSWORD = "test_password"
def fetch_profile(session, user_id):
"""
每个线程执行的任务:使用自己的 session 登录并获取用户信息
"""
try:
# 1. 使用自己的 session 进行登录
print(f"Thread {threading.get_ident()}: Logging in for user {user_id}...")
login_url = "http://httpbin.org/post" # 这里用 httpbin 的 post 接口模拟登录
login_data = {"username": USERNAME, "password": PASSWORD, "user_id": user_id}
# 登录请求
response = session.post(login_url, data=login_data)
response.raise_for_status() # 如果请求失败则抛出异常
# 2. 登录成功后,获取个人资料页面
print(f"Thread {threading.get_ident()}: Fetching profile...")
profile_url = "http://httpbin.org/get"
profile_response = session.get(profile_url)
profile_response.raise_for_status()
print(f"Thread {threading.get_ident()}: Successfully fetched profile. Status: {profile_response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Thread {threading.get_ident()}: An error occurred: {e}")
if __name__ == "__main__":
NUM_THREADS = 5
# 创建并启动多个线程
threads = []
for i in range(NUM_THREADS):
# --- 关键点:每个线程都创建自己的 Session ---
session = requests.Session()
# 创建线程,并将 session 和用户ID作为参数传递
# args 必须是一个元组
t = threading.Thread(target=fetch_profile, args=(session, i))
threads.append(t)
t.start()
# 主线程等待所有子线程完成
for t in threads:
t.join()
print("\nAll threads have finished.")
代码解析:
requests.Session()在循环内:for i in range(NUM_THREADS):循环的每一次迭代,我们都创建了一个全新的session对象,这个对象随后被传递给新创建的线程。- 线程隔离:线程 A 拿到
session_A,线程 B 拿到session_B,它们各自管理自己的 Cookie 和连接池,互不干扰。 - 传递参数:
target=fetch_profile指定了线程要执行的函数,args=(session, i)将每个线程专属的session和一个user_id传递给了fetch_profile函数。
| 特性 | Python 线程 (threading) |
Python Session (requests.Session) |
|---|---|---|
| 目的 | 实现并发,提高程序效率和响应速度。 | 管理和复用 HTTP 连接,保持会话状态(如 Cookie)。 |
| 领域 | 操作系统层面的并发编程。 | 应用层面的 HTTP 通信。 |
| 核心对象 | threading.Thread |
requests.Session |
| 线程安全 | - | 不安全,不能在多线程间直接共享。 |
| 协同方式 | 每个线程创建自己的 Session 对象,以避免数据竞争,保证并发请求的独立性和正确性。 |
线程是舞台,Session 是演员,每个演员(线程)都应该有自己的道具和剧本(Session 对象),这样他们才能在舞台上(并发环境中)独立、正确地表演,而不会互相干扰。
