1. 线程是什么?
在计算机科学中,线程(Thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源,但拥有各自独立的执行栈和程序计数器。
简单来说,你可以把进程想象成一个工厂,而线程就是工厂里的工人。多个工人(线程)可以在同一个工厂(进程)里同时工作,共享工厂的原料(内存)和工具(资源),从而高效地完成任务。
2. 为什么需要线程?
使用线程主要为了解决以下问题:
- 提高程序响应能力:在图形界面(GUI)程序中,主线程负责处理用户交互(如点击按钮),而耗时的操作(如下载文件、复杂计算)可以交给后台线程执行,避免界面"卡死"。
- 充分利用多核 CPU:现代 CPU 通常有多个核心,多线程程序可以将任务分配到不同核心上并行执行,显著提升计算密集型任务的性能。
- 简化模型:对于需要同时处理多个连接的服务端程序(如 Web 服务器),为每个连接创建一个线程,比使用复杂的异步 I/O 模型更直观易懂。
3. Python 中的线程模块:threading
Python 标准库提供了 threading 模块来创建和管理线程。它是基于更低级的 _thread 模块构建的,提供了更高级、更易用的接口。
3.1 创建线程的两种方式
方式一:继承 Thread 类
通过继承 threading.Thread 类并重写 run() 方法来定义线程执行体。
python
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"线程 {self.name} 开始运行")
time.sleep(2) # 模拟耗时操作
print(f"线程 {self.name} 结束运行")
# 创建并启动线程
t1 = MyThread("Thread-1")
t2 = MyThread("Thread-2")
t1.start()
t2.start()
# 等待线程结束
t1.join()
t2.join()
print("主线程结束")
方式二:传入目标函数
将可调用对象(函数)传递给 Thread 构造器。
python
import threading
import time
def worker(name, delay):
print(f"线程 {name} 开始工作,休眠 {delay} 秒")
time.sleep(delay)
print(f"线程 {name} 工作完成")
# 创建线程对象,target 指定要运行的函数,args 指定函数的参数
t1 = threading.Thread(target=worker, args=("A", 3))
t2 = threading.Thread(target=worker, args=("B", 1))
t1.start()
t2.start()
t1.join()
t2.join()
print("所有工作线程执行完毕")
3.2 线程的生命周期与常用方法
start(): 启动线程,使其进入就绪状态。每个线程只能调用一次start()。run(): 线程的入口点方法。通常不需要直接调用,由start()内部调用。join([timeout]): 阻塞调用线程(通常是主线程),直到被调用join()的线程终止。可设置超时时间。is_alive(): 返回线程是否还在运行。name: 线程的名称,可用于调试。daemon: 守护线程属性。如果设置为True,当主线程退出时,该线程会被强制结束,而不管它是否执行完毕。
4. 线程同步与通信
当多个线程访问共享资源(如变量、文件、数据库连接)时,可能会发生竞态条件(Race Condition) ,导致数据不一致。threading 模块提供了多种同步原语来解决这个问题。
4.1 锁(Lock)
锁是最基本的同步机制,一次只允许一个线程进入"临界区"。
python
import threading
# 共享资源
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
lock.acquire() # 获取锁
try:
counter += 1
finally:
lock.release() # 释放锁
# 创建多个线程同时增加计数器
threads = []
for i in range(10):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"最终计数器值: {counter}") # 应该是 1000000
使用 with 语句简化锁操作(推荐):
python
def increment_with():
global counter
for _ in range(100000):
with lock: # 自动获取和释放锁
counter += 1
4.2 信号量(Semaphore)
信号量控制同时访问特定资源的线程数量。
python
import threading
import time
# 模拟一个只有3个座位的资源池
semaphore = threading.Semaphore(3)
def access_resource(thread_id):
with semaphore:
print(f"线程 {thread_id} 获得了资源访问权")
time.sleep(2) # 模拟使用资源
print(f"线程 {thread_id} 释放了资源")
threads = []
for i in range(10):
t = threading.Thread(target=access_resource, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
4.3 条件变量(Condition)
条件变量用于复杂的线程间协调,允许一个线程等待特定条件成立,并由其他线程通知它。
python
import threading
import time
import random
# 生产者-消费者模型
queue = []
MAX_ITEMS = 5
condition = threading.Condition()
def producer():
for i in range(10):
time.sleep(random.random())
with condition:
while len(queue) == MAX_ITEMS:
print("队列已满,生产者等待...")
condition.wait() # 释放锁并等待
item = f"产品-{i}"
queue.append(item)
print(f"生产者生产了: {item}")
condition.notify() # 通知等待的消费者
def consumer():
for _ in range(10):
time.sleep(random.random() * 2)
with condition:
while len(queue) == 0:
print("队列为空,消费者等待...")
condition.wait() # 释放锁并等待
item = queue.pop(0)
print(f"消费者消费了: {item}")
condition.notify() # 通知等待的生产者
# 启动生产者和消费者线程
p = threading.Thread(target=producer)
c = threading.Thread(target=consumer)
p.start()
c.start()
p.join()
c.join()
5. 全局解释器锁(GIL)与多线程的局限性
Python(特指 CPython 解释器)有一个著名的特性------全局解释器锁(Global Interpreter Lock, GIL)。GIL 确保同一时刻只有一个线程在执行 Python 字节码。
5.1 GIL 的影响
- I/O 密集型任务:线程在等待 I/O(如网络请求、文件读写)时会释放 GIL,因此多线程能有效提升 I/O 密集型程序的性能。
- CPU 密集型任务:由于 GIL 的存在,多线程无法利用多核 CPU 实现真正的并行计算。对于纯计算任务,多线程可能比单线程更慢(因为线程切换有开销)。
5.2 如何绕过 GIL?
- 使用多进程(
multiprocessing模块):每个进程有独立的 Python 解释器和内存空间,因此没有 GIL 的限制,能真正利用多核 CPU。 - 使用 C 扩展:在 C 扩展中释放 GIL,执行计算密集型操作。
- 使用其他 Python 实现:如 Jython(基于 JVM)或 IronPython(基于 .NET),它们没有 GIL。
- 使用异步编程(
asyncio):对于高并发的 I/O 密集型任务,异步模型比多线程更高效、资源占用更少。
6. 实战:多线程下载器
下面是一个简单的多线程文件下载器示例,演示了线程池的基本用法。
python
import threading
import requests
from queue import Queue
import os
class Downloader:
def __init__(self, num_workers=4):
self.num_workers = num_workers
self.queue = Queue()
self.lock = threading.Lock()
def download_file(self, url, save_path):
"""下载单个文件"""
try:
response = requests.get(url, stream=True, timeout=30)
response.raise_for_status()
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
with self.lock:
print(f"下载成功: {save_path}")
return True
except Exception as e:
with self.lock:
print(f"下载失败 {url}: {e}")
return False
def worker(self):
"""工作线程函数"""
while True:
item = self.queue.get()
if item is None: # 终止信号
self.queue.task_done()
break
url, save_path = item
self.download_file(url, save_path)
self.queue.task_done()
def add_task(self, url, save_path):
"""添加下载任务"""
self.queue.put((url, save_path))
def run(self):
"""启动下载器"""
threads = []
for _ in range(self.num_workers):
t = threading.Thread(target=self.worker)
t.start()
threads.append(t)
# 等待所有任务完成
self.queue.join()
# 发送终止信号给工作线程
for _ in range(self.num_workers):
self.queue.put(None)
for t in threads:
t.join()
print("所有下载任务完成!")
# 使用示例
if __name__ == "__main__":
downloader = Downloader(num_workers=3)
# 添加下载任务(示例URL)
tasks = [
("https://example.com/file1.zip", "file1.zip"),
("https://example.com/file2.pdf", "file2.pdf"),
("https://example.com/file3.jpg", "file3.jpg"),
]
for url, path in tasks:
downloader.add_task(url, path)
downloader.run()
7. 总结与最佳实践
-
明确任务类型:
- I/O 密集型(网络、磁盘):多线程通常有效。
- CPU 密集型 (计算、数据处理):考虑多进程或
concurrent.futures模块。
-
优先使用高层抽象 :对于简单的并行任务,优先使用
concurrent.futures.ThreadPoolExecutor,它提供了更简洁的接口。 -
避免共享状态 :尽量设计无共享数据的线程,通过队列(
queue.Queue)传递数据,这是最安全的并发模型。 -
必须共享时,做好同步 :使用锁、信号量等同步原语保护共享资源,并优先使用
with语句确保锁被正确释放。 -
小心死锁:避免多个线程互相等待对方释放锁的情况。按固定顺序获取锁,或使用带超时的锁。
-
善用守护线程:对于后台支持性任务(如日志、心跳),可设置为守护线程,这样主程序退出时它们会自动结束。
Python 线程是处理并发任务的强大工具,理解其原理、局限性和同步机制,是编写健壮、高效并发程序的关键。