多线程设备的初始化与最佳实践
在多线程设备的开发中,设备的启动和初始化过程涉及到特定线程的操作,同时遵循一些设计模式和最佳实践,能让开发更加高效和稳定。
线程间通信的Open/Close方法
对于多线程设备,启动和初始化工作通常需要在特定线程中完成。以PIM恒温器应用为例,其持久存储子系统在独立的NVRAM线程中运行。该子系统启动时,需要注册对持久存储的模型点变更通知,而模型点的订阅机制要求订阅操作和回调操作在同一线程中进行,因此持久存储子系统的启动代码必须在NVRAM线程中执行。
Open/Close方法是同步的线程间通信(ITC)调用。当主线程调用open()方法时,会向NVRAM线程的ITC消息队列发送消息,并唤醒NVRAM线程。NVRAM线程接收到消息后,调用持久存储子系统进行初始化,完成后将消息返回给主线程。由于open ITC消息是同步的,主线程会阻塞直到收到返回消息。这种同步机制确保了主线程能按顺序完成模块或子系统的初始化。
close()方法则是open()方法的逆操作,用于优雅地关闭模块或子系统。在PIM恒温器示例中,close()方法用于取消对模型点变更通知的订阅。同样,close ITC调用也具有同步语义,当对模块A的close()调用完成后,系统可以认为模块A已完全关闭。
下面是一个使用open/close ITC调用进行模块线程内启动和关闭的示例流程图:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(Main):::process -->|open()| B(Module A):::process
A -->|open()| C(Module Z):::process
B -->|waitForShutdownSignal()| D(shutdownSignal):::process
C -->|waitForShutdownSignal()| D
D -->|close()| B
D -->|close()| C
B -->|inThreadShutdown()| E(End):::process
C -->|inThreadShutdown()| E
主模式(Main Pattern)
主模式指出,应用程序是通过将独立的组件和模块连接在一起构建的。它主要包括以下几个方面:
-
接口引用与具体实现的解析
:确保接口能正确对应到具体的实现。
-
初始化和关闭顺序
:规定各个组件和模块的启动和关闭顺序。
-
可选的组件和模块排序(或运行时执行)
:可以对整个应用、子系统、功能等进行排序。
主模式通常还负责创建要连接的组件和模块,它有两种变体:Main minor和Main major。
Main minor
Main minor是将主模式应用于某个功能或子系统。以Storm::Thermostat::Algorithm类为例,它是HVAC控制算法功能的Main minor模式实现。在使用Main模式时,不一定要在类或命名空间中使用“Main”这个术语。对于Main minor变体,建议使用功能或子系统的名称作为包含实现类和文件的命名空间。
Algorithm类的主要功能包括:
-
创建控制对象集合
:创建Component、Equipment和Equipment::Stage等实例。
-
连接控制对象
:通过提供模型点和控制对象引用,将这些对象连接起来。
-
执行初始化序列
:对控制对象进行初始化。
-
管理运行时执行顺序
:控制对象按特定顺序每两秒执行一次。
该算法类支持特定的HVAC配置,如单级空调与熔炉(最多三级加热)、独立熔炉(最多三级加热)。如果要支持其他配置,需要创建不同的算法类。
以下是Algorithm类相关的类图示例:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(Algorithm):::process -->|executes| B(Component):::process
A -->|executes| C(Equipment):::process
A -->|executes| D(Equipment::Stage):::process
A -->|uses| E(Cpl::Dm::MailboxServer):::process
A -->|has| F(Model Point):::process
B -->|has references to| F
C -->|has references to| F
D -->|has references to| F
A -->|selects| F
Main major
Main major是将主模式应用于创建整个应用程序,旨在将应用程序的平台无关代码绑定到特定平台。在实践中,一些平台特定的绑定会在编译或链接时完成,而Main major模式主要关注运行时的创建和初始化。
嵌入式应用程序的执行通常遵循以下顺序:
1. 执行复位向量的中断服务程序。
2. 执行编译器包含的C/C++运行时代码,包括静态变量初始化、静态分配的C++对象构造函数执行和可能的最小板级初始化。
3. 调用main()函数,一般会执行以下操作:
- 完成所有剩余的板级或BSP初始化。
- 初始化操作系统/实时操作系统(OS/RTOS),线程调度器可能此时启动也可能不启动。
- 将控制权交给应用程序。
- 初始化并启动各个子系统、组件和模块。
- 应用程序运行,此时应用程序已完全构建和初始化,中断启用,线程调度器运行(如果有OS/RTOS)。
Main major模式在步骤3中发挥作用。步骤3.a和3.b涉及平台创建和初始化,而步骤3.c是将应用程序的子系统、组件和模块连接起来。为了便于代码复用,应将步骤3.a和3.b的代码与步骤3.c的代码分开。
下面是PIM恒温器应用在目标硬件和PC上作为功能模拟器运行时,Main major模式实现的伪代码:
// targetMain.cpp
main()
{
startFreeRTOS();
initializeCplLibrary();
initializeRemainingHardware();
infd = ArduinoSerailObject();
outfd = ArduinoSerialObject();
runTheApplication(infd, outfd);
}
// simulatorMain.cpp
main()
{
initializeCplLibrary();
infd = stdin;
outfd = stdout;
runTheApplication(infd, outfd);
}
// commonMain.cpp
runTheApplication( cli_infd, cli_outfd )
{
initializeModelPoints();
initializePlatform0();
startCLIShell( infd, outfd );
createAlgorthimThread( algo_ );
openPlatform0();
algo_.open();
waitForShutdown();
algo_.close();
closePlatform0();
exitPlatrom();
}
// simulatorPlatform.cpp
initializePlatform0()
{
createHouseSimulator();
createNVRamThread( rec_ );
}
openPlatform0()
{
rec_.open();
}
closePlatform0()
{
rec_.close();
}
exitPlatform()
{
exit();
}
// targetPlatform.cpp
initializePlatform0()
{
createNVRamThread( rec_ );
}
openPlatform0()
{
rec_.open();
}
closePlatform0()
{
rec_.close();
}
exitPlatform()
{
vTaskEndSchedular();
}
相关文件在PIM源代码树中的位置如下表所示:
| 图表文件 | 实际文件 |
| — | — |
| targetMain.cpp | projects/Storm/Thermostat/adafruit-grand-central-m4/windows/gcc/main.cpp |
| simulatorMain.cpp | projects/Storm/Thermostat/simulation/windows|linux/main.cpp |
| commonMain.cpp | src/Storm/Thermostat/Main/Main.cpp |
| targetPlatform.cpp | src/Storm/Thermostat/Main/_adafruit-grand-central-m4/platform.cpp |
| simulatorPlatform.cpp | src/Storm/Thermostat/Main/_simulation/platform.cpp |
最佳实践
在嵌入式项目开发中,还需要遵循一些最佳实践,以提高代码的质量和稳定性。
避免动态内存分配
在嵌入式项目中,使用动态内存(从堆中分配内存)存在一些问题:
-
内存耗尽
:嵌入式设备的内存堆有限,长时间运行可能导致内存耗尽。
-
内存碎片
:反复的分配和释放操作会导致堆内存碎片化,即使总内存足够,也可能因没有足够大的连续内存块而导致分配失败。
-
非确定性
:“堆分配”和“堆释放”操作的时间是不确定的,分配操作还有可能失败。
因此,最佳实践是在应用程序启动后避免使用动态内存分配。可以在启动时从堆中分配内存,但系统运行后应避免。不过,不使用动态内存分配并不意味着不能使用动态对象。应用程序可以为特定的动态对象集合提供私有内存池,确保内存池不会碎片化,且分配和释放操作的时间是有界的。
此外,容器(如链表、排序列表、字典等)可以使用侵入式列表(intrusive listing)来构建,这样即使不使用动态内存分配,容器也能容纳无限数量的元素(假设内存无限)。Colony.core C++库就广泛使用了侵入式容器。
头文件文档化
头文件应像“数据手册”一样,详细记录模块或类的语法、语义、用法以及注意事项,让头文件的使用者无需反编译.c|.cpp文件就能理解如何使用。具体建议如下:
-
完整记录头文件
:例如,以下是src/Cpl/System/Timer.h头文件的文档示例,展示了如何记录语义、用法、约束和线程行为等:
/** This mostly concrete interface defines the operations that can be
performed on a software timer. Software timers execute 'in-thread'
in that all operations (start, timer expired callbacks, etc.) are
performed in a single thread.
Because the timer context (i.e. the timer owner),
timer methods and callbacks all occur in the same thread, the
timer context will never receive a timer expired callback AFTER
the timer's stop() method has been called.
NOTES:
o The timer context must implement the following method:
virtual void expired( void ) noexcept;
o Because the timing source of an individual thread may
NOT be a clean divider of the timer duration, the timer
duration is taken as the minimum. For example: if the
timing source has a resolution of 20msec per count, and the
timer duration on the start() timer call is 5 msec, then the
timer will expire after the next full count, i.e. after
20msec, not 5msec. IT IS THE APPLICATION'S RESPONSIBILITY TO
MANAGE THE RESOLUTION OF THE TIMING SOURCES.
*/
class Timer {
public:
/// Constructor
Timer( TimerManager& timingSource );
/** Constructor. Alternate constructor - that defers the
assignment of the timing source
*/
Timer();
public:
/** Starts the timer with an initial count down count duration of
'timerDurationInMilliseconds'. If the timer is currently
running, the timer is first stopped, and then restarted.
*/
virtual void start( unsigned long timerDurationInMilliseconds );
/** Stops the timer. It is okay to call stop() even after the
timer has previously expired or explicitly stopped.
*/
virtual void stop();
public:
/** Callback notification of the timer’s start time expiring. The
Timer is placed in the stopped state when the start time
expires. The Timer’s ‘Context’ (aka a child class) is
responsible for implementing this method.
*/
virtual void expired( void ) = 0;
public:
/** Sets the timing source. This method CAN ONLY BE CALLED
when the timer has never been started or it has been
stopped
*/
virtual void setTimingSource( TimerManager& timingSource );
};
- 避免重复注释 :不要将头文件中的注释复制到.c|.cpp文件中。如果不确定注释应放在哪里,就放在头文件中。
- 注释命名空间 :在每个命名空间目录中创建README.txt文件,记录该命名空间中所有模块的语义和用法。
- 使用工具提取信息 :使用Doxygen或类似工具从头文件和命名空间特定的README.txt文件中提取信息,并以可搜索、超链接的格式呈现,这可以作为正式详细设计文档的补充,确保代码级细节始终是最新的。
通过遵循这些模式和最佳实践,可以提高嵌入式项目的开发效率和代码质量,减少潜在的问题和错误。
多线程设备的初始化与最佳实践
避免动态内存分配的进一步探讨
前面提到了在嵌入式项目中动态内存分配存在的问题以及应对的最佳实践,下面进一步深入探讨避免动态内存分配后的一些细节和相关操作。
当我们决定不使用动态内存分配后,为特定的动态对象集合提供私有内存池是关键。以下是一个简单的伪代码示例,展示如何实现一个私有内存池:
// 定义一个简单的内存池类
template <typename T, size_t N>
class MemoryPool {
private:
T buffer[N];
bool used[N];
public:
MemoryPool() {
for (size_t i = 0; i < N; ++i) {
used[i] = false;
}
}
T* allocate() {
for (size_t i = 0; i < N; ++i) {
if (!used[i]) {
used[i] = true;
return &buffer[i];
}
}
return nullptr; // 没有可用内存
}
void free(T* ptr) {
for (size_t i = 0; i < N; ++i) {
if (&buffer[i] == ptr) {
used[i] = false;
break;
}
}
}
};
在实际应用中,可以这样使用这个内存池:
// 使用内存池
MemoryPool<int, 10> pool;
int* ptr = pool.allocate();
if (ptr) {
*ptr = 42;
// 使用ptr
pool.free(ptr);
}
使用私有内存池的优点在于可以避免内存碎片,并且分配和释放操作的时间是可预测的。同时,使用侵入式列表构建容器时,需要注意元素只能同时存在于一个容器中,但这个问题并不常见,并且可以通过代理模式等方法解决。
头文件文档化的重要性及实践补充
头文件文档化是提高代码可维护性和可读性的重要手段。除了前面提到的详细记录头文件、避免重复注释、注释命名空间和使用工具提取信息外,还可以进一步探讨其在团队协作和项目开发中的重要性。
在团队开发中,清晰的头文件文档可以让不同成员更快地理解和使用其他模块的代码。例如,当新成员加入项目时,通过阅读头文件的文档,能够快速了解模块的功能和使用方法,减少学习成本。
以下是一个使用Doxygen工具生成文档的简单步骤:
1.
安装Doxygen
:根据不同的操作系统,选择合适的安装方式进行安装。
2.
创建Doxygen配置文件
:在项目根目录下,使用命令
doxygen -g
生成默认的配置文件
Doxyfile
。
3.
配置Doxyfile
:打开
Doxyfile
,根据项目需求进行配置,例如指定输入文件路径、输出目录等。
4.
添加文档注释
:按照前面提到的方式,在头文件中添加详细的文档注释。
5.
生成文档
:在命令行中运行
doxygen Doxyfile
,Doxygen会根据配置文件和注释生成相应的文档。
综合实践案例分析
为了更好地理解前面提到的多线程设备初始化、设计模式和最佳实践,我们以一个简化的嵌入式项目为例进行分析。
假设我们正在开发一个智能家居设备,该设备具有多个传感器和执行器,并且需要与云端进行通信。以下是项目的主要模块和设计思路:
模块划分
- 传感器模块 :负责采集环境数据,如温度、湿度等。
- 执行器模块 :根据控制指令控制设备的开关、调节等操作。
- 通信模块 :与云端进行数据交互,上传传感器数据并接收控制指令。
- 控制算法模块 :根据传感器数据和预设规则生成控制指令。
设计模式应用
- Main major模式 :将项目的平台无关代码与特定平台进行绑定。在启动时,完成硬件初始化、操作系统初始化等操作,然后将控制权交给应用程序。
- Main minor模式 :在控制算法模块中应用Main minor模式,创建和管理传感器、执行器等控制对象,确保它们按顺序初始化和执行。
最佳实践遵循
- 避免动态内存分配 :为传感器数据缓存、控制指令队列等动态对象提供私有内存池。
- 头文件文档化 :为每个模块的头文件添加详细的文档注释,方便团队成员理解和使用。
以下是一个简化的代码示例,展示了项目的启动和初始化过程:
// main.cpp
#include "sensor_module.h"
#include "actuator_module.h"
#include "communication_module.h"
#include "control_algorithm.h"
int main() {
// 初始化硬件
initializeHardware();
// 初始化操作系统
initializeOS();
// 创建模块实例
SensorModule sensor;
ActuatorModule actuator;
CommunicationModule communication;
ControlAlgorithm control;
// 初始化模块
sensor.initialize();
actuator.initialize();
communication.initialize();
control.initialize();
// 启动通信模块
communication.start();
// 主循环
while (true) {
// 读取传感器数据
sensor.readData();
// 执行控制算法
control.process(sensor.getData());
// 执行器执行指令
actuator.execute(control.getCommand());
// 上传数据到云端
communication.sendData(sensor.getData());
}
return 0;
}
通过这个案例可以看出,合理应用设计模式和最佳实践可以使项目结构清晰、易于维护,并且提高代码的可靠性和稳定性。
总结
在多线程设备的开发中,线程间通信的Open/Close方法确保了模块的有序初始化和关闭,主模式(Main minor和Main major)为应用程序的构建提供了清晰的框架,而避免动态内存分配和头文件文档化等最佳实践则有助于提高代码的质量和可维护性。通过深入理解和应用这些知识,可以在嵌入式项目开发中少走弯路,提高开发效率,减少潜在的问题和错误。在实际项目中,应根据具体需求灵活运用这些模式和实践,不断优化和改进代码,以满足项目的各种要求。
超级会员免费看
10万+

被折叠的 条评论
为什么被折叠?



