第1章:Python线程基础——threading模块核心API、创建线程的两种方式、线程的启动与等待、线程名称与标识

各位同学,咱们今天正式开始聊多线程。说实话,我刚开始接触工控机编程那会儿,对线程也是又爱又怕。爱的是它能并行干活,怕的是稍不留神就出bug。但别担心,跟着我一步步来,你会发现线程其实没那么玄乎。

1.1 threading模块:Python的线程工具箱

Python标准库里的threading模块,说白了就是官方给我们准备好的线程工具箱。你想想看,如果没有这个模块,我们得自己写底层线程管理代码,那得多痛苦?

我个人习惯,在项目里第一件事就是:

import threading

就这么一行,整个线程世界的大门就打开了。这个模块提供了什么?我列一下核心内容:

  • Thread类:创建线程的基石
  • Lock、RLock:线程同步锁,防止数据打架
  • Event、Condition:线程间通信的信号灯
  • Semaphore:控制同时访问资源的线程数量
  • Timer:延迟执行任务的线程

嗯,这里要注意,工控场景下最常用的其实是ThreadLock。我在做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-1Thread-2。但说实话,这种名字在调试时基本没用。我建议在工控项目里,给每个线程起个有意义的名称,比如PLC_Read_ThreadMotor_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组合起来,就能搭建一个简单的工控数据采集系统了。

好了,第一章的内容就到这里。线程的基础知识就像盖房子的地基,一定要打牢。下一章我们会聊线程同步,那才是真正考验技术的地方。各位同学,先动手把今天的代码跑一遍,有问题随时交流。