第1章:Python线程基础——threading模块核心API、创建线程的两种方式、线程的启动与等待、线程名称与标识
各位同学,咱们今天正式开始聊多线程。说实话,我刚开始接触工控机编程那会儿,对线程也是又爱又怕。爱的是它能并行干活,怕的是稍不留神就出bug。但别担心,跟着我一步步来,你会发现线程其实没那么玄乎。
1.1 threading模块:Python的线程工具箱
Python标准库里的threading模块,说白了就是官方给我们准备好的线程工具箱。你想想看,如果没有这个模块,我们得自己写底层线程管理代码,那得多痛苦?
我个人习惯,在项目里第一件事就是:
import threading
就这么一行,整个线程世界的大门就打开了。这个模块提供了什么?我列一下核心内容:
- Thread类:创建线程的基石
- Lock、RLock:线程同步锁,防止数据打架
- Event、Condition:线程间通信的信号灯
- Semaphore:控制同时访问资源的线程数量
- Timer:延迟执行任务的线程
嗯,这里要注意,工控场景下最常用的其实是Thread和Lock。我在做PLC数据采集项目时,90%的线程问题都靠这两个搞定。
1.2 创建线程的两种方式
创建线程有两种主流方式,我分别说说。
方式一:直接实例化Thread类
这种方式最直观,适合简单的任务。你只需要把要执行的函数传进去就行。
import threading
import time
def read_sensor():
"""模拟读取传感器数据"""
for i in range(5):
print(f"传感器读数: {i}")
time.sleep(0.5)
# 创建线程
t = threading.Thread(target=read_sensor)
t.start()
t.join()
print("主线程结束")
我在项目中遇到过一种情况:工控机需要同时读取多个传感器的数据。如果用单线程,得一个一个等,效率太低了。这时候直接创建多个线程,每个线程负责一个传感器,效果立竿见影。
方式二:继承Thread类并重写run方法
这种方式更灵活,适合复杂的任务。你可以把线程封装成一个类,方便管理状态和数据。
import threading
import time
class SensorThread(threading.Thread):
def __init__(self, sensor_id):
super().__init__()
self.sensor_id = sensor_id
self.data = []
def run(self):
"""线程执行体"""
for i in range(5):
value = i * 10 + self.sensor_id
self.data.append(value)
print(f"传感器{self.sensor_id} 读数: {value}")
time.sleep(0.3)
# 使用
t1 = SensorThread(1)
t2 = SensorThread(2)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"传感器1数据: {t1.data}")
print(f"传感器2数据: {t2.data}")
我个人更推荐第二种方式,尤其是在工控项目中。为什么?因为你可以把传感器的配置、状态、数据都封装在类里,代码结构清晰,后期维护也方便。
核心区别总结:
| 对比项 | 直接实例化 | 继承Thread类 |
|---|---|---|
| 适用场景 | 简单任务,函数式编程 | 复杂任务,面向对象编程 |
| 代码复用 | 较低 | 较高,可封装属性和方法 |
| 数据管理 | 需通过全局变量或参数传递 | 可直接使用实例属性 |
| 工控推荐度 | ⭐⭐ | ⭐⭐⭐⭐ |
1.3 线程的启动与等待
创建完线程,接下来就是启动和等待。这两个操作看似简单,但坑不少。
启动线程:start()
调用start()方法,线程才会真正开始执行。注意,不是run()!
t = threading.Thread(target=my_func)
t.start() # 正确:启动线程
# t.run() # 错误:这只是在主线程中调用函数,不会创建新线程
我曾经见过新手直接调run(),结果发现程序还是顺序执行的,百思不得其解。记住,start()才是启动线程的正确姿势。
等待线程:join()
join()方法让主线程等待子线程执行完毕。这在工控场景下特别重要——比如你要等所有传感器都采集完数据,才能进行下一步处理。
threads = []
for i in range(3):
t = threading.Thread(target=worker, args=(i,))
t.start()
threads.append(t)
# 等待所有线程完成
for t in threads:
t.join()
print("所有线程执行完毕,开始汇总数据")
这里有个细节:join()可以设置超时时间。比如t.join(2.0)表示最多等2秒,超时就不等了。我在做实时控制系统时经常用这个,防止某个线程卡死导致整个系统挂掉。
避坑指南:我曾经在项目中忘记调用join(),结果主线程提前退出,子线程还没跑完就被强制终止了。那一次排查了好久才发现问题。所以,记得在需要同步的地方加上join()。
1.4 线程名称与标识
当你的程序里有几十个线程同时跑时,怎么区分它们?这就需要线程名称和标识了。
线程名称:name
每个线程都可以有一个名字,方便调试和日志记录。
def worker():
print(f"当前线程: {threading.current_thread().name}")
t1 = threading.Thread(target=worker, name="传感器采集线程")
t2 = threading.Thread(target=worker, name="数据处理线程")
t1.start()
t2.start()
输出效果:
当前线程: 传感器采集线程
当前线程: 数据处理线程
如果不指定名称,Python会自动分配一个,比如Thread-1、Thread-2。但说实话,这种名字在调试时基本没用。我建议在工控项目里,给每个线程起个有意义的名称,比如PLC_Read_Thread、Motor_Control_Thread,这样看日志一目了然。
线程标识:ident
ident是线程的唯一标识符,由Python自动分配,不会重复。
t = threading.Thread(target=worker)
t.start()
print(f"线程标识: {t.ident}")
这个标识在底层调试时很有用,比如你想确认某个线程是否还活着,可以检查它的ident是否还在活跃线程列表里。
注意事项:线程的ident只有在start()之后才会被赋值。如果你在创建线程后立即访问ident,得到的是None。我在调试时踩过这个坑,查了半天才发现是访问时机不对。
1.5 实战小案例:模拟工控数据采集
最后,咱们来个综合小案例,把今天学的都用上。
import threading
import time
import random
class DataCollector(threading.Thread):
"""数据采集线程"""
def __init__(self, channel_name):
super().__init__(name=f"{channel_name}_采集线程")
self.channel = channel_name
self.data = []
def run(self):
print(f"[{self.name}] 开始采集")
for i in range(3):
value = random.uniform(20.0, 30.0)
self.data.append(value)
print(f"[{self.name}] 第{i+1}次采集: {value:.2f}")
time.sleep(random.uniform(0.5, 1.0))
print(f"[{self.name}] 采集完成")
# 创建多个采集线程
channels = ["温度传感器", "压力传感器", "流量传感器"]
threads = []
for ch in channels:
collector = DataCollector(ch)
threads.append(collector)
collector.start()
# 等待所有线程完成
for t in threads:
t.join()
# 汇总结果
print("\n=== 采集汇总 ===")
for t in threads:
print(f"{t.name}: 数据点数{len(t.data)}, 平均值{sum(t.data)/len(t.data):.2f}")
print("主程序结束")
这个案例里,我们用到了继承Thread类、自定义线程名称、启动线程、等待线程、获取线程数据。你看,把这些基础API组合起来,就能搭建一个简单的工控数据采集系统了。
好了,第一章的内容就到这里。线程的基础知识就像盖房子的地基,一定要打牢。下一章我们会聊线程同步,那才是真正考验技术的地方。各位同学,先动手把今天的代码跑一遍,有问题随时交流。