Python多线程基础:从并发加速到线程安全与锁机制
在处理高延迟的 I/O 任务或需要并发执行的操作时,合理使用多线程能显著提升程序效率。本文通过实际代码演示如何从零构建多线程应用,逐步解决常见的资源竞争与状态同步问题,并提供基于 Lock 的安全并发方案。
利用多线程优化 I/O 密集型任务
当程序频繁依赖外部网络或磁盘读写时,CPU 会在等待响应时处于空闲状态。通过启动多个线程并行处理请求,可以有效掩盖等待时间,从而缩短整体运行周期。
第1步:编写单线程顺序执行对比基准
目的:观察单线程依次处理网络请求时的累计耗时。此时代码按顺序阻塞等待每个接口的返回值。
import time
import urllib.request
def get_responses():
urls = [
'http://www.baidu.com',
'http://www.amazon.com',
'http://www.ebay.com',
'http://www.alibaba.com',
'https://www.jb51.net'
]
start = time.time()
for url in urls:
print(url)
resp = urllib.request.urlopen(url)
print(resp.getcode())
print("Elapsed time: %s" % (time.time() - start))
get_responses()
执行后各链接依次返回状态码(如 200),总耗时为各次请求延迟的累加值。这说明在单线程模型下,CPU 无法在等待 A 接口返回时去请求 B 接口。
第2步:改造为多线程并发处理逻辑
目的:继承 Thread 类并重写 run 方法,使每个 URL 分配独立线程并行发起请求,大幅压缩总耗时。
import time
import urllib.request
from threading import Thread
class GetUrlThread(Thread):
def __init__(self, url):
self.url = url
super(GetUrlThread, self).__init__()
def run(self):
resp = urllib.request.urlopen(self.url)
print(self.url, resp.getcode())
def get_responses_thread():
urls = [
'http://www.baidu.com',
'http://www.amazon.com',
'http://www.ebay.com',
'http://www.alibaba.com',
'https://www.jb51.net'
]
start = time.time()
threads = []
for url in urls:
t = GetUrlThread(url)
threads.append(t)
t.start() # 通知操作系统调度当前线程执行 run()
for t in threads:
t.join() # 主线程等待所有子线程执行完毕后再继续
print("Elapsed time: %s" % (time.time() - start))
get_responses_thread()
并发执行后,返回状态码的顺序不再固定,因为 CPU 切换调度的时机不可预测。但总耗时显著下降,证明多线程成功让 CPU 在等待网络响应时分心处理其他任务。注意 join() 的使用保证了计时器在所有请求完成后才触发。
排查共享变量的资源竞争问题
当多个线程共同访问并修改同一份数据时,极易产生并发修改冲突(Race Condition)。以下示例展示全局变量为何无法累积到预期值,并给出标准修复方案。
第3步:复现全局变量并发修改的竞争异常
from threading import Thread
some_var = 0
class IncrementThread(Thread):
def run(self):
global some_var
read_value = some_var
print("some_var in %s is %d" % (self.name, read_value))
some_var = read_value + 1
print("some_var in %s after increment is %d" % (self.name, some_var))
def use_increment_thread():
threads = []
for i in range(50):
t = IncrementThread()
threads.append(t)
t.start()
for t in threads:
t.join()
print("After 50 modifications, some_var should have become 50")
print("After 50 modifications, some_var is %d" % some_var)
use_increment_thread()
多次运行会发现最终结果往往远小于 50。原因是在某个线程读取了旧值(如 15)后,CPU 可能立即切换至另一个线程,后者读到的仍是 15。两者分别加 1 后再写入,覆盖彼此的结果,导致数据丢失。这就是典型的线程不安全场景。
第4步:使用 Lock 锁定实现原子化操作
from threading import Lock, Thread
lock = Lock()
some_var = 0
class IncrementThread(Thread):
def run(self):
global some_var
lock.acquire() # 获取锁,独占临界区
read_value = some_var
print("some_var in %s is %d" % (self.name, read_value))
some_var = read_value + 1
print("some_var in %s after increment is %d" % (self.name, some_var))
lock.release() # 释放锁,允许其他线程进入
def use_increment_thread():
threads = []
for i in range(50):
t = IncrementThread()
threads.append(t)
t.start()
for t in threads:
t.join()
print("After 50 modifications, some_var should have become 50")
print("After 50 modifications, some_var is %d" % some_var)
use_increment_thread()
加入 Lock 后,任意时刻仅有一个线程能执行读取、计算、写回的全流程。这确保了共享状态的逻辑原子性,多次运行均可稳定得到预期结果 50。在实际工程中,更推荐使用 with lock: 语法自动管理锁的释放。
验证线程隔离性与并发输出保护
Python 中每个线程拥有独立的调用栈,实例变量天然具备线程局部特性(Thread-local),不同线程不会互相篡改彼此的对象属性。但非局部的 I/O 输出仍可能被交织,需额外保护。
第5步:验证线程私有变量与打印同步机制
from threading import Thread, Lock
import time
lock = Lock()
class CreateListThread(Thread):
def run(self):
self.entries = []
for i in range(10):
time.sleep(1) # 挂起当前线程,强制触发上下文切换
self.entries.append(i)
lock.acquire() # 保护共享的输出设备(终端)
print(self.entries)
lock.release()
def use_create_list_thread():
for i in range(3):
t = CreateListThread()
t.start()
use_create_list_thread()
若不加锁直接 print,由于三个线程交替执行,终端可能出现多段列表内容交叉拼凑的乱序现象。加上 Lock 后,打印操作成为串行化动作,确保每次输出的都是完整列表。该案例证明了线程间无法直接修改对方的局部变量,仅需关注对外部资源的同步访问。
总结
Python 多线程能够高效突破 I/O 瓶颈,但在共享内存交互时必须警惕竞态条件。掌握 threading.Thread 的生命周期控制、理解线程私有存储区的边界,并熟练使用 Lock 保护临界区,是编写稳定并发程序的基石。在实际业务中,若涉及大量 CPU 密集计算,还需结合 multiprocessing 模块绕过解释器的 GIL 限制。以上就是Python多线程从并发加速到线程安全的详细内容,更多关于Python基础开发技巧的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









