嵌入式C语言中的面向对象与多线程编程

2017-05-12 09:41茅胜荣肖家文乔东海
单片机与嵌入式系统应用 2017年5期
关键词:信号量子类多态

茅胜荣,肖家文,乔东海

(苏州大学 电子信息学院,苏州 215006)

嵌入式C语言中的面向对象与多线程编程

茅胜荣,肖家文,乔东海

(苏州大学 电子信息学院,苏州 215006)

面向对象与多线程编程的诞生大大提高了软件开发的效率,降低了开发复杂应用的难度,但在一些小型的嵌入式系统中很难得到应用,其中最重要的限制因素就是微控制器的代码空间有限,使得适用于计算机的软件架构不适合嵌入式设备。本文将介绍一种能够使用在嵌入式场合的面向对象与多线程的编程机制,通过构造特殊的宏定义来模拟面向对象和多线程编程的软件环境,开销小,效率高。以此为基础进行二次开发的软件代码可读性和移植性更好,可以加快嵌入式软件的更新与迭代。

嵌入式系统;C语言;面向对象;多线程

引 言

C语言编译器没有提供面向对象的原生支持,因此也没有专门的关键字来表达类、继承等概念,但是面向对象思想与语言本身无关,本文使用C语言以LiteOOC(Lite Object-Oriented C)为名实现了一套面向对象机制,通过宏定义巧妙地表达了封装、继承和多态的思想。本文将详细介绍LiteOOC的底层实现原理。

现代操作系统的重要特点之一是支持多线程编程,但是在没有底层操作系统支持的嵌入式软件中,一般的做法是使用一个大循环,例如单片机中的while(1)或者Arduino中的loop()函数,通过轮询的方式来执行不同的任务。但任务之间的同步与互斥难以实现,并且代码可读性不高。而移植操作系统不但使工作变得复杂,还会增加对Flash和RAM资源的消耗,不适合一些资源稀缺的应用。本文将介绍一个低开销的并发程序设计机制——Protothreads,它利用宏定义为C语言模拟了一种无堆栈的轻量级线程环境。

1 LiteOOC介绍

使用C语言来进行面向对象编程需要解决两大问题,一是如何表达面向对象中的基本要素,如封装、继承和多态;二是如何妥善管理内存,当程序中的对象之间有组合、聚合等关系时,如果没有一个有效的内存管理方法,就很容易造成内存泄露或者内存访问异常。

1.1 基本思想

LiteOOC通过宏定义对结构体封装,巧妙地实现了封装、继承与多态。LiteOOC采用引用计数的方式来管理存在的对象,只有当对象的引用计数为0时,调用delete函数才会释放内存资源。LiteOOC维护着一个如下所示的对象记录链表:

/* 对象记录链表的每个成员*/

typedef struct LOOC_MemAllocUnit_s{

char file[LOOC_MAX_PATH]; //文件名

int line; //行号

void* addr; //内存地址

size_t size; //内存块大小

struct LOOC_MemAllocUnit_s* next;//下一个内存块记录

} LOOC_MemAllocUnit;

存放用户调用looc_malloc函数分配的内存信息,包括调用内存分配函数的文件名、内存地址、内存大小等,虽然这会额外消耗一定的内存资源,但是在开发初期非常有助于监视内存泄露的情况。另外在LiteOOC中,所有的类都继承自同一个父类——loocObject,利用多态的性质,loocObject类型的指针可以指向所有的对象。

1.2 封 装

封装,即隐藏对象的属性和实现细节,仅公开必要的接口给用户读取和修改对象的属性。封装可以增强安全性、简化编程,使用者不必去了解具体的实现细节。在LiteOOC中,使用如下所示的宏定义来模拟C++、Java等面向对象语言中的类:

/* 抽象类*/

#define ABS_CLASS(type)

typedef struct type##_s type;

extern void type##_ctor(type* cthis);

extern void type##_dtor(type* cthis);

extern void type##_delete(type* cthis);

struct type##_s

/* 具体类*/

#define CLASS(type)

typedef struct type##_s type;

extern type* type##_new(looc_file_line_param);

extern void type##_ctor(type* cthis);

extern void type##_dtor(type* cthis);

extern void type##_delete(type* cthis);

struct type##_s

实质上是利用结构体来封装对象的属性与方法。通过给CLASS宏传递一个类名,将会同时声明以该类名为前缀的new、delete、ctor和dtor函数,它们分别相当于C++中的new关键字、delete关键字、构造函数和析构函数。与此同时,因为抽象类不能被直接实例化,所以ABS_CLASS宏中没有声明new函数。

C++在堆上实例化一个类需要使用new关键字,编译器会立即为该对象分配内存空间,然后调用构造函数完成对象的初始化。相应地,在LiteOOC中使用如下所示的CTOR宏定义来实现对象的new函数,new函数中先调用looc_malloc为对象分配内存,并将地址返回给cthis指针,接着调用对象的构造函数ctor完成对象的初始化工作。ctor函数的具体实现细节需要由用户在CTOR与END_CTOR两个宏之间完成。

#define CTOR(type)

type* type##_new(const char* file, int line){

type* cthis;

cthis = (type*)looc_malloc(sizeof(type),#type,file,line);

if(!cthis){

return NULL;

}

type##_ctor(cthis);

return cthis;

}

void type##_ctor(type* cthis){

#define END_CTOR

}

C++中释放堆上对象的内存空间需要使用delete关键字,编译器会自动释放当前对象的内存,然后调用析构函数完成资源的回收。相应地,在LiteOOC中使用如下所示的DTOR宏来实现对象的delete函数:

#define DTOR(type)

void type##_delete(type* cthis){

type##_dtor(cthis);

looc_free((void*)cthis);

}

void type##_dtor(type* cthis){

#define END_DTOR

}

delete函数中先调用对象的析构函数dtor,回收对象的资源,然后调用looc_free释放记录在对象链表中的内存空间。dtor函数的具体实现细节需要由用户在DTOR和END_DTOR两个宏之间完成。

1.3 继 承

面向对象中的继承机制通过扩展原有的类、声明新的类来实现,使得代码具有可重用性和扩展性。在LiteOOC中,通过如下所示的EXTENDS宏来模拟继承的思想,其功能是将父类在子类中实例化,且EXTENDS只能在CLASS宏内部使用。

#define EXTENDS(type) struct type##_s type

1.4 多 态

在面向对象机制中,同一方法在子类和父类中的行为是不同的,方法的行为取决于调用方法的对象,这种行为即为多态。多态性允许将父类对象的指针指向子类对象,父类对象指针根据当前指向的子类对象的特性以不同的方式运作。在LiteOOC中,实现多态需要分三步:首先子类需要继承某个抽象类,然后重写其抽象方法,最后将实现的方法绑定在抽象类上。所谓绑定,在LiteOOC中是指将具体实现的函数地址赋给类的成员方法。使用如下所示的FUNCTION_SETTING宏可以实现函数绑定的功能:

#define FUNCTION_SETTING(f1,f2)

cthis->f1 = f2

该宏必须在CTOR和END_CTOR宏之间调用。LiteOOC中的析构函数是多态的,用户调用父类的delete函数将会释放父类对象指针指向的子类对象的内存空间,本文第2.2节会通过例子来分析具体的实现过程。

使用多态的过程中常需要做类型转换,LiteOOC提供了向上类型转换SUPER_PTR的宏:

#define SUPER_PTR(cthis, father)

((father*)(&(cthis->father)))

向下类型转换SUB_PTR的宏:

#define SUB_PTR(selfptr,self,child)

((child*)((char*)selfptr-looc_offsetof(child,self)))

向上类型转换将子类对象cthis转换成father类型的对象。向下类型转换将self类型的父类对象selfptr转换成child类型的对象。

1.5 loocObject类

loocObject类是LiteOOC中所有类的父类,如下所示:

ABS_CLASS(loocObject){

/*引用计数*/

int _use;

/*子类通过覆写finalize方法,实现对资源清理行为的定制*/

void (*finalize)(loocObject* object);

};

该类只有两个成员:一是引用计数,用来管理何时释放内存资源,只有当引用计数为0时才能将对象析构;二是finalize方法,是一个抽象方法,需要子类重写来实现具体的析构操作。loocObject类的实现如下所示,在构造函数中,将对象的引用计数清零,在析构函数中,调用了finalize方法。

ABS_CTOR(loocObject)

/*初始引用计数为0*/

cthis->_use = 0;

END_ABS_CTOR

DTOR(loocObject)

/*调用子类自定义的finalize方法*/

cthis->finalize(cthis);

END_DTOR

2 在LiteOOC上实现队列

队列是嵌入式编程中常用的数据结构之一,本节将介绍如何在LiteOOC的基础上,利用封装、继承、多态的思想实现队列的基本操作。

2.1 loocQueue类的声明

CLASS(loocQueue) {

/*继承自loocObject*/

EXTENDS(loocObject);

/*队列最大的大小*/

int _maxSize;

/*队列中每个元素的大小*/

int _elementSize;

/*队首*/

int front;

/*队尾*/

int rear;

/*队列有效长度*/

int length;

/*内存池*/

void* queue_pool;

/*初始化一个队列*/

void (*init)(loocQueue* cthis, int maxSize, int elementSize);

/*入队操作*/

looc_bool (*enqueue)(loocQueue* cthis, void* data);

/*出队操作*/

void* (*dequeue)(loocQueue* cthis);

};

以上是队列类loocQueue的声明,实际上是利用宏CLASS声明了一个类型为loocQueue的结构体,以及loocQueue_new、loocQueue_delete、loocQueue_ctor和loocQueue_dtor四个函数。在类的内部,使用EXTENDS宏继承了loocObject类,定义了队列的成员变量如队列首尾元素索引front和rear、队列的有效长度length等,还声明了队列的成员方法,如入队操作enqueue和出队操作dequeue。

2.2 loocQueue类的定义

类的定义分三步完成。一要实现类的成员方法,例如loocQueue_enqueue函数实现了类的enqueue方法,如果继承了抽象类,那么还需要重写抽象方法,例如loocQueue_finalize函数重写了父类loocObject的finalize方法。二要在构造函数中初始化成员变量并且绑定成员方法。三要在析构函数中完成类的析构操作。loocQueue类的构造函数如下所示:

CTOR(loocQueue)

/*调用父类的构造函数*/

SUPER_CTOR(loocObject);

cthis->_elementSize = 1;

cthis->_maxSize = LOOC_QUEUE_DEFAULT_SIZE;

cthis->front = 0;

cthis->rear = 0;

cthis->length = 0;

/*成员函数的绑定*/

FUNCTION_SETTING(init, loocQueue_init);

FUNCTION_SETTING(enqueue, loocQueue_enqueue);

FUNCTION_SETTING(dequeue, loocQueue_dequeue);

FUNCTION_SETTING(loocObject.finalize, loocQueue_finalize);

END_CTOR

首先调用父类的构造函数,初始化父类对象,接着初始化子类的成员变量,最后绑定类的成员方法。需要注意的是,loocQueue_finalize重写了父类loocObject的抽象方法,所以需要绑定在父类上。loocQueue类的析构函数如下所示,里面向上调用了父类的析构函数。

DTOR(loocQueue)/*调用父类的析构函数,实质上就是子类实现的finalize方法*/

SUPER_DTOR(loocObject);

END_DTOR

LiteOOC中的析构函数是多态的,如图1所示,假如程序中除了有队列loocQueue,还存在链表loocList,他们都继承自loocObject抽象类,且对象都通过SUPER_PTR宏向上类型转换成loocObject类型的对象,当调用loocObject类的finalize方法时,前者会执行loocQueue_finalize函数,后者会执行loocList_finalize函数。这是因为在使用new函数实例化对象的时候已经在构造函数中将loocQueue_finalize绑定在了父类的finalize上,用户调用loocObject类的finalize方法就是调用与之绑定在一起的函数。这便是LiteOOC实现多态的精髓所在——函数绑定。

图1 析构函数的多态

2.3 loocQueue类的使用

loocQueue的测试程序如下所示:

/*创建队列对象*/

loocQueue* queue = loocQueue_new(looc_file_line);

/*初始化队列*/

queue->init(queue, 10, sizeof(int));

/*入队操作*/

for (i = 0; i < 15; i++){

queue->enqueue(queue, (void*) &i);

}

/*出队操作*/

for (i = 0; i < 10; i++){

printf("%d ", *(int*) queue->dequeue(queue));

}

printf(" ");

/*释放队列内存空间*/

loocQueue_delete(queue);

/*报告内存泄漏情况*/

looc_report();

使用loocQueue_new函数来创建一个队列对象queue,该函数会间接调用loocQueue_ctor,完成对象成员方法的绑定和成员变量的初始化。调用类的enqueue方法来实现入队的操作,调用类的dequeue方法来实现出队的操作,调用loocQueue_delete函数可以回收queue对象的内存空间。程序的最后,利用LiteOOC提供的looc_report函数可以报告此时的内存泄漏情况,方便用户排错。

3 Protothreads介绍

Protothreads为C语言模拟了一种无堆栈的轻量级线程环境,能够实现线程的条件阻塞、信号量等操作系统中特有的机制。Protothreads目前已经成为物联网操作系统Contiki的一部分,同时也是Arduino中普遍使用的多线程库。严格意义上来讲,Protothreads是一种协程而非线程,一个程序可以包含多个协程,这类似一个进程包含多个线程。因为小型嵌入式系统程序往往是单进程的,所以也可以将Protothreads称为线程。

3.1 基本思想

Protothreads的实现全部在头文件中使用宏定义封装,本质上是构造了C语言的状态机模型,巧妙地利用了switch语句能够随意跳转进入if和while控制语句内部的优点。Protothreads将程序跳转的行号作为状态切换的标记,程序如下所示:

#define LC_INIT(s) s = 0;

#define LC_RESUME(s) switch(s) { case 0:

#define LC_SET(s) s = __LINE__; case __LINE__:

#define LC_END(s) }

3.2 基本操作

Protothreads中的线程控制结构体pt被定义为一个16位的整型,用来保存状态机跳转时的行号。在线程调度前,需要使用PT_INIT宏将pt初始化为0,复位状态机。Protothreads中的线程执行函数的返回值必须是整型,且线程的开始和结束都必须在PT_BEGIN与PT_END两个宏之间。程序如下所示:

#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)

#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0;

PT_INIT(pt); return PT_ENDED; }

图2 生产者和消费者模型

PT_BEGIN设置了PT_YIELD_FLAG标志,表示当前线程可以让出控制权,然后又会进入switch判断语句,根据当前pt的值跳转到上一次线程退出的地方继续执行。PT_END用来结束当前的switch判断语句,并将PT_YIELD_FLAG标志清零,把当前pt的值也清零,返回当前状态机的状态。因为Protothreads切换线程的时候不会保存上下文,所以线程函数中不允许使用局部变量,但可以通过静态局部变量或者全局变量来代替。

Protothreads中让出线程控制权的方法主要有以下几个函数:PT_WAIT_UNTIL(pt, condition)函数会根据condition条件是否成立来条件阻塞当前线程;PT_WAIT_THREAD(pt, thread)函数会一直等待直到子线程thread结束;PT_YIELD_UNTIL(pt, condition)函数会立即退出当前线程,直到condition条件成立,它与PT_WAIT_UNTIL的差别在于,第一次调用PT_YIELD_UNTIL函数一定会退出线程,而不管condition条件是否成立。

3.3 互斥与同步

多线程编程中的互斥是指临界资源同一时刻只允许一个访问者对其进行访问,具有唯一性和排他性,但是互斥无法限制访问者对资源的访问顺序。同步是指在互斥的基础上,实现访问者对资源的有序访问。Protothreads使用信号量来实现互斥与同步,信号量是一个32位无符号整型数,值为正时,说明资源空闲,若为0说明资源被占用。Protothreads中对信号量的操作有3种:PT_SEM_INIT(s, c)函数用来初始化一个信号量,s代表信号量结构体指针,c表示信号量的初始化值。PT_SEM_WAIT(pt, s)函数执行“等信号”操作,如果此时信号量为0,线程将阻塞,直到信号量大于0,否则将信号量减1,线程继续往下执行。PT_SEM_SIGNAL(pt, s)函数执行“给信号”操作,直接将信号量加1,然后线程继续往下执行。

4 在Protothreads上实现生产者-消费者模型

生产者和消费者模式能够解决大多数并发问题,该模型通过平衡生产线程和消费线程的工作能力来提高程序的整体处理速度。如图2所示,定义了两个信号量:full和empty。前者代表待生产的资源数,后者代表可供消费的资源数。生产者线程会先检查full信号量是否为0,如果等于0,则表示暂时不需要生产,线程阻塞,直到消费者调用PT_SEM_SIGNAL(pt,&full)。消费者线程会先检查empty信号量是否为0,如果等于0,则表示暂时没有资源可供消费,线程阻塞,直到生产者调用PT_SEM_SIGNAL(pt,&empty)。可见利用Protothreads的同步与互斥功能能够快速高效地实现生产者-消费者模型。

结 语

[1] 高焕堂.UML+OOPC嵌入式C语言开发精讲[M].北京:电子工业出版社,2008.

[2] Mark Allen Weiss.数据结构与算法分析:C语言描述[M].北京:机械工业出版社,2010.

[3] Dunkels.Protothreads-Lightweight,Stackless Threads in C[EB/OL].[2017-02].http://dunkels.com/adam/pt/.

[4] Contiki-os.org.Contiki:The Open Source Operating System for the Internet of Things[EB/OL].[2017-02].http://www.contiki-os.org/.

茅胜荣、肖家文(在校研究生):研究方向为嵌入式系统设计、信号处理;乔东海(教授),研究方向为信号处理、MEMS器件设计。

Object-oriented and Multi-thread Programming in Embedded C Language

Mao Shengrong,Xiao Jiawen,Qiao Donghai

(Shool of Electronic Information,Soochow University,Suzhou 215006,China)

Object-oriented and multi-thread programming greatly enhance the efficiency of software development,and reduce the difficulty to develop complex applications,but it is difficult to apply in some small embedded systems.The most important constraint is the small space of microcontroller and the software architecture which is suitable for computer but unfit for embedded devices.In the paper,an object-oriented and multi-thread programming mechanism is introduced which can be used in the embedded environment.By constructing special macro definitions,the object-oriented and multi-thread ideology can be simulated,which is known for low overhead and high efficiency.Using this construction,the software will have higher code readability and portability,so that we can accelerate the upgrade and iteration of the embedded software.

embedded system;C language;object-oriented;multi-thread

TP311.1

A

士然

2017-02-06)

猜你喜欢
信号量子类多态
分层多态加权k/n系统的可用性建模与设计优化
卷入Hohlov算子的某解析双单叶函数子类的系数估计
参差多态而功不唐捐
Nucleus PLUS操作系统信号量机制的研究与测试
关于对称共轭点的倒星象函数某些子类的系数估计
硬件信号量在多核处理器核间通信中的应用
人多巴胺D2基因启动子区—350A/G多态位点荧光素酶表达载体的构建与鉴定及活性检测
μC/OS- -III对信号量的改进
Linux操作系统信号量机制的实时化改造
烟碱型乙酰胆碱受体基因多态与早发性精神分裂症的关联研究