网络与并发编程|第七部分:Python标准库精选 (The Standard Library)

发布于 2025-09-11 分类: Python
系列文章: Python全方位教程
第 1 部分: Python语言概览|第一部分:Python入门与环境搭建 (Python Foundations) 第 2 部分: 搭建开发环境|第一部分:Python入门与环境搭建 (Python Foundations) 第 3 部分: Python基本语法与规范|第一部分:Python入门与环境搭建 (Python Foundations) 第 4 部分: 数据类型深入解析|第二部分:Python核心数据类型与运算符 (Core Data Types & Operators) 第 5 部分: 运算符大全|第二部分:Python核心数据类型与运算符 (Core Data Types & Operators) 第 6 部分: 条件与循环|第三部分:流程控制与数据结构操作 (Control Flow & Data Structures Manipulation) 第 7 部分: 数据结构高级操作|第三部分:流程控制与数据结构操作 (Control Flow & Data Structures Manipulation) 第 8 部分: 函数|第四部分:函数、模块与代码组织 (Functions, Modules & Code Organization) 第 9 部分: 模块与包|第四部分:函数、模块与代码组织 (Functions, Modules & Code Organization) 第 10 部分: 类与对象|第五部分:面向对象编程 (Object-Oriented Programming - OOP) 第 11 部分: OOP核心特性|第五部分:面向对象编程 (Object--Oriented Programming - OOP) 第 12 部分: 高级OOP主题|第五部分:面向对象编程 (Object-Oriented Programming - OOP) 第 13 部分: 迭代与生成|第六部分:高级Python编程 (Advanced Python) 第 15 部分: 内存管理与性能|第六部分:高级Python编程 (Advanced Python) 第 16 部分: 文件与目录操作|第七部分:Python标准库精选 (The Standard Library) 第 17 部分: 数据处理与序列化|第七部分:Python标准库精选 (The Standard Library) 第 18 部分: 网络与并发编程|第七部分:Python标准库精选 (The Standard Library) (当前) 第 20 部分: 系统交互|第七部分:Python标准库精选 (The Standard Library) 第 21 部分: 数据科学与分析入门|第八部分:Python生态与实战应用 (Ecosystem & Applications) 第 22 部分: Web开发入门|第八部分:Python生态与实战应用 (Ecosystem & Applications) 第 23 部分: GUI编程入门|第八部分:Python生态与实战应用 (Ecosystem & Applications) 第 24 部分: 图像处理入门|第八部分:Python生态与实战应用 (Ecosystem & Applications) 第 25 部分: 自动化脚本|第八部分:Python生态与实战应用 (Ecosystem & Applications) 第 26 部分: Python备忘单 (Cheatsheet)|第九部分:附录与资源 (Appendix & Resources) 第 27 部分: 常见面试题与解答|第九部分:附录与资源 (Appendix & Resources) 第 28 部分: 官方文档与其他学习资源链接|第九部分:附录与资源 (Appendix & Resources)

欢迎来到Python的“高速公路”和“多核处理器”时代!到目前为止,我们编写的程序大多是单线程的、顺序执行的。就像一个勤奋但只有一个人的工坊,一次只能做一件事,做完一件再做下一件。

但是,现代计算机拥有多个CPU核心,网络请求也充满了等待。如果我们能让程序在等待网络响应的时候,去做别的事情,或者同时利用多个CPU核心来并行处理任务,程序的效率将会得到极大的提升。这就是并发编程的魅力。

本章,我们将探索两个激动人心的主题:

  • 网络编程: 我们将学习如何使用 urllibsocket 模块,让你的Python程序能够像浏览器一样发送网络请求,获取网页内容,或者构建底层的网络服务。
  • 并发编程 (多线程): 我们将深入 threading 模块,学习如何创建和管理多个线程,让你的程序能够同时执行多个任务。你将理解线程的生命周期、如何保证数据在多线程下的安全(),以及如何避免可怕的死锁

掌握了这些,你的程序将不再是一个孤立的、单任务的个体,而是一个能够连接世界、高效利用资源的现代化应用。

18.1 urllibsocket 模块

urllib:简单高效的HTTP客户端

urllib 是Python内置的HTTP请求库,它包含多个模块,其中 urllib.request 是最常用的,可以让你轻松地像浏览器一样访问网页。

注意: 在实际项目中,更推荐使用第三方库 requests,它的API设计得更友好、更简洁。但了解标准库的 urllib 依然非常重要。

from urllib import request, parse, error

# --- 1. 发送一个简单的 GET 请求 ---
url = "https://www.python.org"
try:
    with request.urlopen(url) as response:
        # response 对象类似于一个文件对象
        print(f"状态码: {response.status}")
        print(f"响应头: \n{response.getheaders()}")
        
        # 读取内容 (内容是 bytes 类型,需要解码)
        html_content = response.read().decode('utf-8')
        print("\n--- 网页内容 (前100个字符) ---")
        print(html_content[:100])

except error.URLError as e:
    print(f"访问URL时出错: {e.reason}")

# --- 2. 发送一个带参数的 GET 请求 ---
# 比如搜索 ?q=python
base_url = "https://www.bing.com/search"
params = {"q": "python tutorial"}
query_string = parse.urlencode(params) # 将字典编码成 "q=python+tutorial"
full_url = f"{base_url}?{query_string}"

print(f"\n构造的URL: {full_url}")
# 后续的请求方式同上

socket:网络编程的基石

urllib 是高层封装,而 socket 模块则提供了底层的、通用的网络套接字接口。它是所有网络通信(包括HTTP、FTP、SMTP等)的基础。使用 socket,你可以编写自己的服务器和客户端。

这是一个非常底层的模块,我们通过一个极简的“回声服务器”来感受一下它的工作流程:

# --- 服务器端 (echo_server.py) ---
import socket

# 1. 创建 socket 对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定地址和端口
server_socket.bind(('127.0.0.1', 9999))
# 3. 开始监听连接
server_socket.listen(5)
print("服务器已启动,等待客户端连接...")

while True:
    # 4. 接受客户端连接 (这是一个阻塞操作)
    client_socket, addr = server_socket.accept()
    print(f"接收到来自 {addr} 的连接")
    
    # 5. 接收数据
    data = client_socket.recv(1024) # 一次最多接收1024字节
    print(f"收到数据: {data.decode('utf-8')}")
    
    # 6. 发送数据 (将收到的数据原样返回)
    client_socket.send(data)
    
    # 7. 关闭客户端连接
    client_socket.close()
# --- 客户端 (echo_client.py) ---
import socket

# 1. 创建 socket 对象
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务器
client_socket.connect(('127.0.0.1', 9999))

# 3. 发送数据
message = "Hello, Server!"
client_socket.send(message.encode('utf-8'))

# 4. 接收服务器返回的数据
response = client_socket.recv(1024)
print(f"收到服务器的回应: {response.decode('utf-8')}")

# 5. 关闭连接
client_socket.close()

如何运行: 先运行 echo_server.py,然后再运行 echo_client.py,你就能看到它们之间完成了通信。

18.2 并发编程: 多线程 (threading 模块)

线程是操作系统能够进行运算调度的最小单位。一个进程(比如你运行的Python程序)可以包含多个线程,它们共享进程的内存空间,可以同时执行不同的任务。

多线程的适用场景: 主要用于 I/O密集型 任务。比如,当你的程序需要等待网络响应、等待文件读写时,CPU是空闲的。多线程可以让CPU在这段空闲时间里去执行其他线程的任务,从而提高整体效率。

注意: 由于Python的全局解释器锁(GIL),在同一时刻,一个Python进程中只有一个线程能真正执行Python字节码。因此,多线程对于CPU密集型任务(如大规模科学计算)并不能实现真正的并行加速。对于CPU密集型任务,应该使用多进程 (multiprocessing 模块)

线程生命周期与创建

import threading
import time

def worker(name, delay):
    """一个简单的线程工作函数"""
    print(f"线程 {name}: 开始工作...")
    time.sleep(delay)
    print(f"线程 {name}: 工作完成。")

# --- 创建和启动线程 ---
# 创建 Thread 对象,target 指定线程要执行的函数,args 是传递给函数的参数 (必须是元组)
thread1 = threading.Thread(target=worker, args=("A", 2))
thread2 = threading.Thread(target=worker, args=("B", 4))

print("主线程: 准备启动子线程。")
# 启动线程
thread1.start()
thread2.start()

print("主线程: 子线程已启动,我继续做别的事情...")

# 等待子线程结束
thread1.join() # join() 会阻塞主线程,直到 thread1 执行完毕
thread2.join()

print("主线程: 所有子线程都已结束。")

运行以上代码,你会看到线程A和B是“同时”开始的,主线程在它们运行期间也可以继续执行。

线程同步 (锁, 事件, 条件变量)

当多个线程共享和修改同一个数据时,可能会导致数据错乱,这就是线程不安全。想象一下,两个线程同时对一个银行账户余额(初始100)执行 balance += 10,可能会出现一个线程刚读取了100,还没写回110,另一个线程也读取了100,最后两个线程都写回110,结果是110而不是期望的120。

为了解决这个问题,我们需要线程同步机制,最常用的就是锁 (Lock)

balance = 0
lock = threading.Lock() # 创建一个锁对象

def change_balance(n):
    global balance
    # --- 临界区代码 ---
    lock.acquire() # 获取锁,只有一个线程能成功获取,其他线程会在此等待
    try:
        # 这里的操作是线程安全的
        current_balance = balance
        time.sleep(0.01) # 模拟一些耗时操作
        balance = current_balance + n
    finally:
        lock.release() # 必须释放锁,否则其他线程将永远等待

# 创建多个线程同时修改余额
threads = []
for i in range(100):
    t = threading.Thread(target=change_balance, args=(1,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"最终余额: {balance}") # 如果没有锁,结果会不确定;有锁,结果总是 100

线程死锁与避免

死锁是多线程编程中最可怕的问题之一。它指的是两个或多个线程互相持有对方需要的资源,并无限期地等待对方释放,导致所有相关线程都无法继续执行。

经典案例:哲学家就餐问题
想象两个线程(哲学家)A和B,以及两把锁(筷子)L1和L2。

  • 线程A:先获取L1,再尝试获取L2。
  • 线程B:先获取L2,再尝试获取L1。

如果A获取了L1,同时B获取了L2,那么A将永远等待B释放L2,B也永远等待A释放L1,死锁发生!

避免死锁的策略:

  1. 按序加锁: 规定所有线程都必须按照相同的顺序来获取锁(例如,总是先获取L1,再获取L2)。
  2. 设置超时: 在尝试获取锁时使用超时参数 lock.acquire(timeout=...),如果超时还未获取到,就先释放自己已有的锁,稍后再试。

线程池 (concurrent.futures)

频繁地创建和销毁线程是有开销的。线程池是一种预先创建好一定数量的线程,并将任务提交给它们去执行的模式,可以有效地复用线程,管理并发任务。

Python 3.2 引入的 concurrent.futures 模块提供了高级的、易于使用的接口来处理线程池和进程池。

from concurrent.futures import ThreadPoolExecutor
import time

def task(n):
    print(f"正在执行任务 {n}...")
    time.sleep(2)
    return f"任务 {n} 的结果"

# 创建一个最多包含3个线程的线程池
with ThreadPoolExecutor(max_workers=3) as executor:
    # 提交任务到线程池
    future1 = executor.submit(task, 1)
    future2 = executor.submit(task, 2)
    future3 = executor.submit(task, 3)
    future4 = executor.submit(task, 4) # 任务4会等待前3个任务之一完成后才开始

    # 获取任务的结果 (future.result() 是一个阻塞操作)
    print(future1.result())
    print(future2.result())
    print(future3.result())
    print(future4.result())

ThreadPoolExecutor 结合 with 语句使用,可以自动管理线程池的关闭,代码非常简洁优雅。


太了不起了!你现在已经将你的Python技能从本地扩展到了网络,从单核思维升级到了并发思维。你学会了:

  • 如何使用 urllib 获取网络资源。
  • 如何使用 socket 构建底层的网络应用。
  • 多线程的基本概念、创建和管理。
  • 如何使用锁来保证线程安全,并了解了死锁的成因与避免方法。
  • 如何使用现代的线程池来高效地管理并发任务。

你的程序已经具备了现代化应用的核心能力。在下一章,我们将关注程序的“健康与维护”。你将学习如何进行调试、测试与日志记录,这些是保证软件质量、让你的程序在复杂环境中稳定运行的“三大法宝”。准备好,成为一名专业的、负责任的软件工程师吧!


-- 感谢阅读 --