一 本节介绍
📝本节我们将学习 MicroPython _thread 模块的基础用法,包括创建工作线程、使用锁保护共享状态、协作式停止线程,以及安全管理线程所使用的硬件资源。
教程同时适用于以下两款开发板:
- 立创·庐山派K230-CanMV开发板
- 立创·庐山派Lite-K230D-CanMV开发板
1.1 学习目标
🏆学习目标
1️⃣ 理解线程、并发、共享资源和临界区的基本概念。
2️⃣ 掌握 _thread.start_new_thread() 与 _thread.allocate_lock() 的常用方法。
3️⃣ 能使用锁保护共享状态,并让临界区保持足够短。
4️⃣ 理解 _thread 没有高级 join() 和强制终止接口,掌握停止标志、完成标志和超时等待的组合方法。
5️⃣ 能运行一份通过 get_board_info() 自动适配两款开发板的线程例程。
1.2 重点提示
_thread 是低级且实验性的接口
MicroPython 官方将 _thread 标记为高度实验性模块,其 API 和不同端口的行为可能存在差异。本文以 CanMV v1.8 及其对应的 MicroPython e00a144 实现为基准,并只使用已经在源码中确认的基础接口。
IMPORTANT
线程不是“自动提速”按钮。CanMV 线程端口建立在 RT-Smart/pthreads 之上,系统调度器可以交错执行线程;具体并行度还会受到固件构建方式、解释器锁、C 扩展和硬件资源限制影响。无论实际是否并行,都必须假设共享状态可能在任意调度点被其他线程访问。
⚠️硬件资源不能随意跨线程争用
摄像头、显示、KPU、音频、文件系统和 GPIO 等资源不一定支持多个线程同时调用。推荐让一个线程独占一个复杂硬件资源,其他线程通过锁保护的状态或消息进行协作。确实需要共享时,必须查清模块的线程安全边界并进行真板压力测试。
1.3 本节效果
运行完整例程后,主线程创建一个工作线程。工作线程独占板载绿色 LED,每隔 300 ms 翻转一次,并在锁保护下更新进度;主线程只读取进度并负责终端输出。
完成 8 次翻转后,工作线程关闭 LED、设置完成标志并退出。主线程确认它已经结束后再执行最终清理。
二 软硬件准备
| 名称 | 数量 | 说明 |
|---|---|---|
| 立创·庐山派K230-CanMV开发板或立创·庐山派Lite-K230D-CanMV开发板 | 1 | 二选一,本教程同时适配 |
| TF 卡 | 1 | 已写入与板卡型号匹配的 CanMV MicroPython 固件 |
| Type-C 数据线 | 1 | 需要支持数据传输,不能只支持充电 |
| 电脑 | 1 | 用于连接开发板、运行脚本并查看终端输出 |
| 外接模块 | 0 | 基础实验使用 _thread 和板载绿色 LED |
固件要求:建议使用 CanMV v1.8 或更新版本,并确保固件镜像与板卡型号匹配。
开发环境:可以使用与当前固件配套的 CanMV IDE K230、CanMV IDE Web,或 VS Code 配合 CanMV 扩展。
三 双板兼容说明
| 项目 | 立创·庐山派K230-CanMV开发板 | 立创·庐山派Lite-K230D-CanMV开发板 |
|---|---|---|
| 是否支持本节实验 | 支持 | 支持 |
| 主控 | K230 | K230D |
_thread API | 相同 | 相同 |
| 内存 | 1GB 外置内存 | 128MB 内置 SiP 内存 |
| 板载绿色 LED | GPIO20,低电平点亮 | GPIO66,高电平点亮 |
| 示例线程数量 | 1 个工作线程 + 主线程 | 1 个工作线程 + 主线程 |
| 板卡适配方式 | get_board_info() 自动适配 | get_board_info() 自动适配 |
IMPORTANT
_thread 本身不使用外部引脚,两款开发板的线程接口相同。本教程为了展示跨线程状态同步,使用板载绿色 LED;代码会同时适配 GPIO 编号和有效电平。
Lite-K230D 只有 128MB 内存。线程越多,线程栈、局部对象和共享数据占用的内存越大。不要根据 K230 标准版的余量盲目增加 Lite-K230D 的线程数量。
四 基础知识与名词解释
4.1 基本名词
| 名词 | 英文 | 说明 |
|---|---|---|
| 线程 | Thread | 进程内部的一条执行路径 |
| 主线程 | Main Thread | 执行顶层脚本的线程 |
| 工作线程 | Worker Thread | 由主线程创建、负责特定任务的线程 |
| 并发 | Concurrency | 多个任务在时间上交错推进 |
| 并行 | Parallelism | 多个任务在同一时刻真正执行 |
| 共享资源 | Shared Resource | 可被多个线程访问的变量、缓冲区、文件或外设 |
| 临界区 | Critical Section | 必须互斥访问的一小段代码 |
| 竞态条件 | Race Condition | 结果依赖线程交错顺序而产生的不确定错误 |
| 死锁 | Deadlock | 线程互相等待资源,导致所有相关任务都无法继续 |
4.2 并发不等于并行
线程可以让等待网络、文件、音频或其他阻塞操作的任务与主流程交错推进,这属于并发。CPU 密集型 Python 代码能否并行加速,则取决于解释器锁、底层 C 模块是否释放锁、RT-Smart 调度和硬件资源。
因此,不要仅因为 K230/K230D 是多核芯片,就假设两个 Python 线程一定各占一个核心并获得两倍性能。是否值得使用线程,应以真板测量结果为准。
4.3 线程调度不是简单的“主动休息才会切换”
旧教程常把 MicroPython 线程描述成“非抢占式,必须主动 sleep() 才会切换”。这个说法不适用于当前 CanMV 端口:CanMV v1.8 的线程端口使用 pthreads,底层由 RT-Smart 调度,线程执行可以被交错调度。
time.sleep_ms() 仍然非常重要,因为它可以避免无任务时持续忙等待,并给其他线程和系统服务留下运行时间;但它不是线程发生切换的唯一条件。
4.4 为什么共享变量需要锁
下面的“读取—修改—写回”并不是一个不可分割的动作:
counter = counter + 1一个线程可能刚读完旧值就被切换,另一个线程也读到同一个旧值,最终造成更新丢失。列表、字典、缓冲区和外设状态也存在类似风险。
锁可以把关键操作变成临界区,保证同一时刻只有一个线程进入:
lock.acquire()
try:
counter = counter + 1
finally:
lock.release()2
3
4
5
4.5 锁的工程使用原则
- 临界区只放必须互斥的状态读写。
- 不在持锁期间执行
sleep()、网络请求、文件读写、传感器采集或复杂显示刷新。 - 获取锁后使用
try/finally,确保异常时也能释放。 _thread的基础锁不可重入,同一线程不要重复获取同一把锁。- 多把锁必须固定获取顺序,避免线程 A 等待 B、线程 B 又等待 A。
4.6 线程生命周期与协作式退出
当前 _thread.start_new_thread() 返回线程标识,但不是可调用 join() 的线程对象;模块也没有可靠的“从外部强制杀死指定线程”接口。
本教程使用三件套管理生命周期:
stop_requested:主线程请求工作线程停止。worker_done:工作线程在finally中确认自己已经退出。- 等待超时:防止主线程无限等待一个失去响应的工作线程。
_thread.exit() 只让调用它的当前线程退出,不能用来结束另一个线程。通常让线程函数正常返回更加清晰。
4.7 子线程异常不会自动传递给主线程
MicroPython 的低级线程入口会打印未捕获异常并结束该工作线程,但主线程不会像调用普通函数那样自动收到这个异常。
本教程在工作线程中捕获异常,把错误文本写入锁保护的共享状态,再由主线程检查并报告。这样可以避免“子线程已经悄悄退出,主线程仍以为系统正常”的问题。
4.8 硬件资源采用单一所有者
本例把绿色 LED 的所有权交给工作线程:
- 工作线程负责翻转 LED,并在退出前关闭 LED。
- 主线程不在工作线程运行期间操作 LED。
- 主线程只有确认
worker_done后,才执行兜底关闭。
这种“一个资源、一个所有者”的模型,比在多个线程外层套一把大锁更容易维护,也能显著降低死锁和时序错误。
五 常用 API 说明
5.1 创建线程
_thread.start_new_thread(function, args)| 参数 | 说明 |
|---|---|
function | 新线程开始执行的函数 |
args | 传给线程函数的位置参数元组;没有参数时必须写成 () |
| 第三个可选参数 | 关键字参数字典;基础教程通常不使用 |
| 返回值 | 新线程的整数标识,不是支持 join() 的线程对象 |
最小示例:
import _thread
import time
def worker(name):
print("工作线程:{}".format(name))
thread_id = _thread.start_new_thread(worker, ("worker-1",))
print("线程标识:{}".format(thread_id))
time.sleep_ms(100)2
3
4
5
6
7
8
9
单元素参数元组需要逗号
("worker-1",) 是一个单元素元组;("worker-1") 只是字符串。参数元组写错会导致线程函数收到错误的参数数量。
5.2 创建锁
lock = _thread.allocate_lock()allocate_lock() 返回一把基础互斥锁。新创建的锁处于未锁定状态。
5.3 获取和释放锁
lock.acquire()
try:
# 只放需要互斥的共享状态操作
pass
finally:
lock.release()2
3
4
5
6
| API | 作用 | 返回值/注意事项 |
|---|---|---|
lock.acquire() | 阻塞等待并获取锁 | 成功返回 True |
lock.acquire(False) | 尝试获取锁,不等待 | 成功返回 True,锁忙返回 False |
lock.release() | 释放已经持有的锁 | 未锁定时释放会抛出 RuntimeError |
lock.locked() | 查询锁是否处于锁定状态 | 返回布尔值;不能替代真正的 acquire() |
当前对应的 MicroPython 源码尚未实现锁的超时等待语义,因此不要依赖 acquire() 的第三个超时参数。本教程只使用阻塞获取,并通过缩短临界区避免长时间等待。
5.4 其他基础接口
| API | 作用 | 使用建议 |
|---|---|---|
_thread.get_ident() | 获取当前线程标识 | 仅用于日志和诊断,不作为资源句柄 |
_thread.exit() | 结束调用它的当前线程 | 通常让线程函数自然返回更清晰 |
_thread.stack_size([size]) | 查询或设置后续线程的请求栈大小 | 与端口实现相关,除非完成内存测量,否则保持默认 |
六 操作步骤
6.1 运行前检查
- 确认 TF 卡固件与实际板卡型号匹配。
- 使用支持数据传输的 Type-C 数据线连接电脑。
- 关闭其他正在控制板载绿色 LED 的脚本。
- 在 CanMV 开发环境中新建脚本,复制第七节完整代码。
- 首次运行保持默认的 1 个工作线程和 8 次循环。
6.2 运行与观察
- 运行脚本。
- 查看终端打印的板卡名称、绿色 LED GPIO 和线程标识。
- 观察绿色 LED 每
300 ms翻转一次。 - 查看主线程输出的进度从
1/8增加到8/8。 - 确认工作线程结束后 LED 熄灭,主线程再退出。
- 再次运行脚本,确认不会残留上一次的线程状态。
七 代码例程
# 立创·庐山派-K230-CanMV开发板资料与相关扩展板软硬件资料官网全部开源
# 开发板官网:www.lckfb.com
# 技术支持常驻论坛,任何技术问题欢迎随时交流学习
# 立创论坛:www.jlc-bbs.com/lckfb
# 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
# 不靠卖板赚钱,以培养中国工程师为己任
# 编写者:LCKFB-YZH
from machine import FPIOA, Pin
import _thread
import os
import time
# ==================== 用户可修改参数 ====================
# 工作线程翻转 LED 的间隔,单位:ms
WORKER_INTERVAL_MS = 300
# 工作线程正常执行的 LED 翻转次数
WORKER_ITERATIONS = 8
# 主线程检查共享状态的周期,单位:ms
MAIN_POLL_INTERVAL_MS = 50
# 主线程请求停止后,等待工作线程退出的最长时间,单位:ms
WORKER_EXIT_TIMEOUT_MS = 3000
# ======================================================
def get_board_info():
"""自动识别庐山派型号,并返回本例需要的 LED 参数。"""
board_id = os.uname()[-1]
if board_id == "k230_canmv_lckfb":
return {
"board_name": "立创·庐山派K230-CanMV开发板",
"LED_G": 20,
"LED_ON_LEVEL": 0,
"LED_OFF_LEVEL": 1,
}
return {
"board_name": "立创·庐山派Lite-K230D-CanMV开发板",
"LED_G": 66,
"LED_ON_LEVEL": 1,
"LED_OFF_LEVEL": 0,
}
def init_green_led(board):
"""初始化板载绿色 LED,并确保初始状态为熄灭。"""
led_pin = board["LED_G"]
fpioa = FPIOA()
fpioa.set_function(
led_pin,
getattr(FPIOA, "GPIO{}".format(led_pin)),
)
led = Pin(
led_pin,
Pin.OUT,
pull=Pin.PULL_NONE,
drive=7,
)
led.value(board["LED_OFF_LEVEL"])
return led
def validate_parameters():
"""在线程启动前检查参数。"""
if WORKER_INTERVAL_MS <= 0:
raise ValueError("WORKER_INTERVAL_MS 必须为正整数")
if WORKER_ITERATIONS <= 0:
raise ValueError("WORKER_ITERATIONS 必须为正整数")
if MAIN_POLL_INTERVAL_MS <= 0:
raise ValueError("MAIN_POLL_INTERVAL_MS 必须为正整数")
if WORKER_EXIT_TIMEOUT_MS <= WORKER_INTERVAL_MS:
raise ValueError("工作线程退出超时必须大于单次工作间隔")
# 所有共享状态都通过同一把锁访问
state_lock = _thread.allocate_lock()
shared_state = {
"stop_requested": False,
"worker_done": False,
"progress": 0,
"worker_error": None,
}
def request_worker_stop():
"""由主线程设置停止请求。"""
state_lock.acquire()
try:
shared_state["stop_requested"] = True
finally:
state_lock.release()
def stop_is_requested():
"""由工作线程读取停止请求。"""
state_lock.acquire()
try:
return shared_state["stop_requested"]
finally:
state_lock.release()
def read_shared_state():
"""一次性复制主线程需要的状态,锁外再进行打印。"""
state_lock.acquire()
try:
return (
shared_state["progress"],
shared_state["worker_done"],
shared_state["worker_error"],
)
finally:
state_lock.release()
def worker_task(green_led, board):
"""工作线程独占绿色 LED,并更新锁保护的进度。"""
led_is_on = False
worker_error = None
try:
for _ in range(WORKER_ITERATIONS):
# 工作线程也保留退出点,便于开发环境停止脚本
os.exitpoint()
if stop_is_requested():
break
# LED 只由本线程访问,不需要使用共享状态锁
led_is_on = not led_is_on
led_level = (
board["LED_ON_LEVEL"]
if led_is_on
else board["LED_OFF_LEVEL"]
)
green_led.value(led_level)
# 临界区只更新共享进度,不在锁内打印或休眠
state_lock.acquire()
try:
shared_state["progress"] += 1
finally:
state_lock.release()
time.sleep_ms(WORKER_INTERVAL_MS)
except BaseException as exc:
# 子线程异常不会自动传给主线程,转成文本保存到共享状态
worker_error = repr(exc)
finally:
# 工作线程释放自己拥有的 LED 状态
try:
green_led.value(board["LED_OFF_LEVEL"])
except Exception as cleanup_exc:
if worker_error is None:
worker_error = "LED 清理异常:{}".format(cleanup_exc)
# 最后一步设置完成标志;主线程看到它后才接管清理
state_lock.acquire()
try:
shared_state["worker_error"] = worker_error
shared_state["worker_done"] = True
finally:
state_lock.release()
BOARD = get_board_info()
green_led = None
worker_started = False
worker_stopped = True
try:
validate_parameters()
green_led = init_green_led(BOARD)
print("当前板卡:{}".format(BOARD["board_name"]))
print("绿色 LED:GPIO{}".format(BOARD["LED_G"]))
thread_id = _thread.start_new_thread(
worker_task,
(green_led, BOARD),
)
worker_started = True
worker_stopped = False
print("工作线程已启动,线程标识:{}".format(thread_id))
last_progress = 0
while True:
os.exitpoint()
progress, worker_done, worker_error = read_shared_state()
if progress != last_progress:
print(
"主线程观察到进度:{}/{}".format(
progress,
WORKER_ITERATIONS,
)
)
last_progress = progress
if worker_done:
worker_stopped = True
if worker_error is not None:
raise RuntimeError("工作线程异常:{}".format(worker_error))
print("工作线程已正常结束")
break
time.sleep_ms(MAIN_POLL_INTERVAL_MS)
except KeyboardInterrupt:
print("用户停止运行")
except Exception as exc:
print("线程示例运行异常:{}".format(exc))
finally:
# 无论主线程如何退出,都先请求工作线程停止
request_worker_stop()
if worker_started and not worker_stopped:
wait_start = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), wait_start) < WORKER_EXIT_TIMEOUT_MS:
_, worker_done, _ = read_shared_state()
if worker_done:
worker_stopped = True
break
time.sleep_ms(MAIN_POLL_INTERVAL_MS)
if not worker_stopped:
print("警告:等待工作线程退出超时")
if green_led is not None:
if not worker_started or worker_stopped:
try:
green_led.value(BOARD["LED_OFF_LEVEL"])
print("绿色 LED 已关闭")
except Exception as cleanup_exc:
print("绿色 LED 关闭异常:{}".format(cleanup_exc))
else:
print("工作线程未确认退出,主线程跳过 LED 操作")
print("程序结束")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
八 代码说明
8.1 板卡自动检测
get_board_info() 同时适配两项差异:
- 立创·庐山派K230-CanMV开发板:GPIO20,低电平点亮。
- 立创·庐山派Lite-K230D-CanMV开发板:GPIO66,高电平点亮。
线程 API 在两板上相同,只有示例使用的 LED 硬件参数需要区分。
8.2 为什么共享状态集中放在字典中
shared_state 明确列出两个线程之间的通信协议:
| 字段 | 写入者 | 读取者 | 作用 |
|---|---|---|---|
stop_requested | 主线程 | 工作线程 | 请求工作线程协作退出 |
worker_done | 工作线程 | 主线程 | 确认工作线程已经完成清理 |
progress | 工作线程 | 主线程 | 报告当前处理进度 |
worker_error | 工作线程 | 主线程 | 把子线程异常传给主线程处理 |
所有字段都在 state_lock 保护下访问,避免依赖偶然的线程执行顺序。
8.3 为什么锁内不打印、不休眠
工作线程更新进度时,临界区只有一行:
state_lock.acquire()
try:
shared_state["progress"] += 1
finally:
state_lock.release()2
3
4
5
LED 操作、print() 和 time.sleep_ms() 都在锁外。这样可以降低锁竞争,也避免一个慢速终端输出阻塞另一个线程读取状态。
8.4 为什么只有工作线程操作 LED
如果主线程和工作线程都随时写同一个 GPIO,即使每次单独写入都合法,最终灯的状态也会依赖调度顺序。
本例采用单一所有者:工作线程运行期间独占 LED;退出时先关闭 LED,再设置 worker_done。主线程看到完成标志后才可以进行兜底清理。
8.5 如何代替 join()
低级 _thread 没有返回可 join() 的线程对象。本例通过完成标志等待:
- 主线程请求停止。
- 工作线程在自己的
finally中释放资源并设置worker_done。 - 主线程轮询完成标志。
- 超过
WORKER_EXIT_TIMEOUT_MS后停止等待并打印警告。
超时只代表主线程不再无限等待,并不会强制杀死工作线程。因此工作线程本身必须避免不可取消的永久阻塞。
8.6 子线程异常处理
工作线程捕获 BaseException,是为了同时记录普通异常以及开发环境注入的退出异常。错误会转换为文本写入 worker_error,工作线程仍然执行 LED 清理并设置完成标志。
主线程发现 worker_error 后,把它转为顶层 RuntimeError 统一输出。实际产品还可以在这里记录日志、触发降级运行或停止喂狗。
8.7 常改参数
| 参数 | 作用 | 建议 |
|---|---|---|
WORKER_INTERVAL_MS | 工作线程任务周期 | 根据任务耗时设置,避免持续忙等待 |
WORKER_ITERATIONS | 演示循环次数 | 使用正整数 |
MAIN_POLL_INTERVAL_MS | 主线程检查状态周期 | 小于工作周期,但不必过度频繁 |
WORKER_EXIT_TIMEOUT_MS | 等待线程协作退出的上限 | 必须大于线程一次最坏阻塞时间 |
九 实际运行效果
以立创·庐山派Lite-K230D-CanMV开发板为例,终端输出形式如下;标准版只会显示不同的板卡名称和绿色 LED GPIO:
当前板卡:立创·庐山派Lite-K230D-CanMV开发板
绿色 LED:GPIO66
工作线程已启动,线程标识:123456789
主线程观察到进度:1/8
主线程观察到进度:2/8
主线程观察到进度:3/8
...
主线程观察到进度:8/8
工作线程已正常结束
绿色 LED 已关闭
程序结束2
3
4
5
6
7
8
9
10
11
线程标识由运行环境分配,每次运行都可能不同。绿色 LED 共翻转 8 次,并在工作线程退出时恢复为熄灭状态。
【TODO】补充双板线程运行效果图 : 分别拍摄两款庐山派绿色 LED 运行状态,并截取主线程进度与工作线程退出日志 🎨 说明:必须使用真板照片和终端截图,不使用 AI 生成;标注 GPIO20/低有效与 GPIO66/高有效的差异
十 常见问题
10.1 为什么两个线程的输出会混在一起?
多个线程同时 print() 时,终端输出顺序取决于调度,复杂字符串甚至可能交错。
建议像完整例程一样让工作线程只更新共享状态,由主线程统一打印。确实需要多线程打印时,可单独设置一把日志锁,但不要在日志锁内执行其他业务。
10.2 最终计数为什么偶尔不正确?
原因:多个线程对共享计数执行读取、修改和写回时没有加锁,更新发生覆盖。
解决方法:
- 用同一把锁保护计数的全部读改写过程。
- 不要只给写入加锁而让相关读取完全无锁。
- 把临界区缩短到最少的状态操作。
10.3 为什么脚本停止后工作线程仍未立即退出?
低级 _thread 没有从外部强制终止指定线程的安全接口。工作线程只有运行到停止检查点或 os.exitpoint() 时才能协作退出。
将永久阻塞调用改为带超时的调用,在线程循环中定期检查 stop_requested,并保证每个线程都能走到自己的 finally。
10.4 程序为什么死锁?
常见原因包括重复获取不可重入锁、异常路径漏掉 release()、持锁执行永久阻塞操作,以及多把锁的获取顺序不一致。
使用 acquire() + try/finally,锁内不休眠、不访问慢速外设,并为整个项目规定统一的锁顺序。
10.5 工作线程报错后,主线程为什么还在运行?
未捕获的子线程异常只会结束该子线程并输出错误,不会自动在主线程中重新抛出。需要像本例一样设置 worker_error 和 worker_done,让主线程明确感知失败。
10.6 为什么增加线程后反而更慢?
CPU 密集型 Python 任务可能受解释器锁、上下文切换、缓存和锁竞争影响;共享 KPU、显示或摄像头时,硬件本身也可能只能串行工作。
先测量单线程基线,再测量多线程吞吐量和延迟。I/O 等待型任务通常更容易从线程并发中获益;纯 Python 计算不应预设能够线性加速。
10.7 Lite-K230D 创建多个线程后内存不足?
Lite-K230D 只有 128MB 内存,每个线程都需要栈空间,线程中的局部对象也会增加垃圾回收压力。
减少线程数量,复用缓冲区,不要在线程循环中持续创建大对象。不要在没有栈使用测量的情况下随意修改 _thread.stack_size()。
10.8 摄像头、显示或 KPU 多线程调用报错?
这些复杂模块通常包含共享驱动、缓冲区和硬件上下文,不能因为 Python 层有锁就默认全部线程安全。
优先让一个线程独占完整的摄像头—推理—显示流水线,其他线程只处理消息或轻量数据。确实需要跨线程调用时,应参考对应官方例程并进行长时间真板压力测试。
十一 总结
本节完成了 _thread 创建、锁保护、异常传递和协作式退出的完整示例。核心要点如下:
- 两款庐山派使用相同的
_threadAPI,示例 LED 通过get_board_info()自动适配。 - CanMV 线程由 RT-Smart/pthreads 调度,不能简单描述成“只有主动休眠才切换”。
- 并发不保证并行,也不保证 CPU 密集型任务提速。
- 共享变量、缓冲区和外设状态必须明确所有权,并在需要时使用锁。
- 临界区应保持短小,不能持锁休眠或执行慢速 I/O。
_thread没有高级join()和安全的外部强制终止接口,应使用停止标志、完成标志和超时等待。- 子线程异常不会自动传给主线程,正式项目必须建立错误反馈通道。
- Lite-K230D 内存更小,应优先减少线程数量并复用缓冲区。
后续可以在掌握这些基础规则后,再学习线程心跳与 WDT 联动、网络接收线程,或官方多线程 AI 示例中的资源互斥设计。