图3.8 消息处理机制
2.消息定义什么是消息?消息只是一个指示,它可以是数字,也可以是字符,还可以是字符串或其他任何形式的标识符。
消息定义的形式与消息检读的方式相对应,通常我们可以将其定义为一些常量,常量可以是各种类型,甚至可以是复合类型。
消息的定义例子如下所示。
#defineMSG_KEY1DOWN 1
#defineMSG_CMD0 "open"
#defineMSG_STATE_OVER 'V'
3.消息处理定义消息的方式并不难,难在如何检读消息、处理消息。
因为在一个并行任务程序中,通常会有很多消息处理的任务,因此消息也绝对不只一个,如何把那些相关的或不相关的消息分门别类地处理好,还真不是一个容易的事情。不过不容易不可怕,可怕的是没有好的魔法。现在我们来修炼消息处理的魔法。
在小规模的消息处理中,可以使用不同的变量来收/发不同类型的消息。
3.10.2 实现小规模消息处理的编程如下所示。
文档: .. 25.c..@Project: 25.uvproj&& Output: none
#include
// 定义消息
#defineMSG_TASK1_RUN 1
#defineMSG_TASK1_STOP 2
#defineMSG_TASK2_RUN 1
#defineMSG_TASK2_STOP 2
main(void)
{
unsigned RandTaskGene1;
unsigned char message1;
unsigned RandTaskGene2;
unsigned char message2;
while(1)
{
RandTaskGene1= rand();
RandTaskGene1%= 2;
if(RandTaskGene1)
{
message1= MSG_TASK1_RUN; // 产生消息
}
else
{
message1= MSG_TASK1_STOP; // 产生消息
}
RandTaskGene2= rand();
if(RandTaskGene2>1000)
{
message2= MSG_TASK2_RUN; // 产生消息
}
else
{
message2= MSG_TASK2_STOP; // 产生消息
}
if(message1) // 消息检读
{
switch(message1)
{
case MSG_TASK1_RUN:
//这里放任务1的运行处理
break;
case MSG_TASK1_STOP:
//这里放任务1的停止处理
break;
}
}
if(message2) // 消息检读
{
switch(message2)
{
case MSG_TASK2_RUN:
//这里放任务2的运行处理
break;
case MSG_TASK2_STOP:
//这里放任务2的停止处理
break;
}
}
}
}
3.10.3 问题分析3.10.2节所示的消息处理机制分类简单,代码易懂,但是重复代码多,这种做法只适合那种消息种类少的应用场合,如果消息种类很多,那么在消息检读时将会出现很多switch语句,这显然不合理,这时就需要另一种消息收发检读策略——消息队列。
在小规模的消息处理中,由于不同类型的消息由不同的变量来指示,所以不同的消息可能会使用相同的编码,如消息MSG_TASK1_RUN与消息MSG_TASK2_RUN的编码都是1。由于它们使用的是不同的编码空间,所以可以使用相同的编码而不会产生消息检读错乱。
在大规模的消息处理中,由于我们将简化代码,不希望出现众多的检读switch,所以不能出现很多的编码空间。理想的编码空间只能有一个,所有消息不分类型,统一编码,在一个统一的检读器中检读。
这种策略需要一个足够长度的队列,以保存随机产生而来不及处理的消息。所有的消息产生都先发送到队列,然后在检读时从队列中按照先入先出的原则来检读并处理它们。
3.10.4 实现大规模消息队列处理的例程如下所示。
文档: .. 26.c..@Project:26.uvproj && Output: none
#include
typedefunsigned char message;
// 定义消息
#define MSG_NONE 0 // 无消息的消息
#define MSG_TASK1_RUN 1
#define MSG_TASK1_STOP 2
#define MSG_TASK2_RUN 3
#define MSG_TASK2_STOP 4
/* ========== 消息处理机制定义 ========== */
// 定义消息
#define QUEUELEN 5 // 消息队列缓冲区大小
unsigned char Messages[QUEUELEN]; // 消息队列缓冲区
unsigned char MessageHead = 0; // 消息队列头
unsigned char MessageTail = 0; // 消息队列尾
/* 队列处理函数 */
// 功能:消息发布
// 参数:m,message类型,要发送的消息
// 返回:无
// 备注:
void PutMessage(message m)
{
Messages[MessageTail] = m;
if(++MessageTail>=QUEUELEN)MessageTail = 0;
}
// 功能:取消息缓冲区中的消息
// 参数:无
// 返回:message类型的消息
// 备注:本函数不检验队列的安全性
// 所以使用之前一定要确认队列不为空
messageGetMessage(void)
{
message m =Messages[MessageHead];
if(++MessageHead>=QUEUELEN) MessageHead = 0;
return m;
}
// 功能:判断队列是否空或出错
// 参数:无
// 返回:0:正常;1:空或出错
// 备注:如果队列头或队列尾相等,则有可能是队列没有消息
// 也有可能是消息过多而导致溢出,如果继续使用将导致信息丢失
// 所以要合理定义队列消息缓冲区的大小
bit QueueEmptyOrError(void)
{
return (MessageHead==MessageTail)?1:0;
}
main(void)
{
unsigned RandTaskGene;
while(1)
{
RandTaskGene = rand();
RandTaskGene %= 2;
if(RandTaskGene)
{
PutMessage(MSG_TASK1_RUN); // 产生消息
}
else
{
PutMessage(MSG_TASK1_STOP); // 产生消息
}
RandTaskGene = rand();
if(RandTaskGene>30000)
{
PutMessage(MSG_TASK2_RUN); // 产生消息
}
else
{
PutMessage(MSG_TASK2_STOP); // 产生消息
}
while(!QueueEmptyOrError()) // 一次性消息检读
{
switch(GetMessage())
{
case MSG_TASK1_RUN:
// 这里放任务1的运行处理
break;
case MSG_TASK1_STOP:
// 这里放任务1的停止处理
break;
case MSG_TASK2_RUN:
// 这里放任务2的运行处理
break;
case MSG_TASK2_STOP:
// 这里放任务2的停止处理
break;
}
}
}
}
3.10.5 回顾与思考在3.10.4节中,有着稍微复杂一些的队列处理机制,这似乎为我们的工作额外增加了一些麻烦,但事实上,使用了这种机制之后,在使用上会无比简单。一旦我们向系统发送了一个消息,我们甚至不用担心消息发到了哪里,由谁来响应消息,我们所要做的,就只是把消息发送出去。消息被发送出去后就进入消息队列排队等候它的主人。排队的消息遵循先进先出的原则,来不及被主人认领的消息将在队列中等候,直到轮到它出队并被主人领走。不同的消息通常会有不同的主人,它们的主人会一直盯着消息队列最前面出队的消息。一旦消息从队列头出来,就会马上被主人响应。消息被响应后就会被遗弃,并把处理机会留给队列中的其他成员。
3.11 广播消息【导读】尽管消息机制只是一个概念,但是对应消息的处理却十分丰富。本节将重点探讨消息是如何广播的。
3.11.1 问题与分析上节讨论的消息在处理完就消失了,发送者与响应者是一对一的对话关系。也许有时候我们会希望全部响应者或某一部分响应者能响应我们的消息。这时我们该怎么做呢?
有一种处理方式叫广播,这正是我们需要的处理机制。消息广播就是把消息发送出去而不论接收者是谁。接收者在受理广播消息时可以有自己的观点,它们完全可以根据自己的需要来决定是否响应这种消息。
对于广播消息的处理机制,我们其实有很多办法可以处理。例如把消息响应的检听者进行分组管理,发送消息时确定检听者的组别。但这样的处理办法需要额外对检听者进行管理并在发送消息时指定检听对象,这是个不利因素。我们还可以对消息进行分组管理,将某一类消息进行连续编号,而检听者在检听时对自己检听范围的消息都进行处理,很显然这需要消息与检听者进行同步管理,这也增加了消息处理的复杂度。我们也可以为消息发送专设广播发送方式,这样消息在队列里除了消息编号外,还有消息的发布方式,不过这时的广播消息只能对所有的检听者都有效,而不能只对其中某一部分检听者有效,而且要准备两套消息管理机制。当然还会有很多其他办法来处理广播消息,不同方式的编程复杂度也不一样,但是一个好的处理机制是需要一个完善的消息发送与检听管理方案的。
广播消息的处理还有一个最显著的特点,就是检听者不能在取消息时擅自删除消息,消息只能等待所有的检听者都检听过后由系统执行删除,3.10.4节中的GetMessage()函数无法胜任广播消息的职责,我们得做一些修改措施。
3.11.2 实现下面便是一个广播消息的例子。
文档: .. 27.c..@Project:27.uvproj && Output: none
#include
typedef unsignedchar message;
// 定义消息
#defineMSG_NONE 0 //无消息的消息
#defineMSG_SING 1
#defineMSG_DANCE 2
#defineMSG_PLAYTHEPIANO 3
#defineMSG_PLAYFOOTBALL 4
#defineMSG_PLAYBASKETBALL 5
#defineMSG_GOTOGAME 6
#
defineMSG_ATTENDMEE
tiNG 7
/* ========== 定义消息处理机制 ========== */
// 定义消息
#define QUEUELEN 5 // 消息队列缓冲区大小
unsigned char Messages[QUEUELEN]; // 消息队列缓冲区
unsigned char MessageHead = 0; // 消息队列头
unsigned char MessageTail = 0; // 消息队列尾
/* 队列处理函数 */
// 功能:消息发布
// 参数:m,message类型,要发送的消息
// 返回:无
// 备注:
void PutMessage(message m)
{
Messages[MessageTail] = m;
if(++MessageTail>=QUEUELEN)MessageTail = 0;
}
// 功能:取消息缓冲区中的消息
// 参数:无
// 返回:message类型的消息
// 备注:本函数不检验队列的安全性
// 所以使用之前一定要确认队列不为空
messageGetMessage(void)
{
message m = Messages[MessageHead];
if(++MessageHead>=QUEUELEN)MessageHead = 0;
returnm;
}
// 功能:判断队列是否空或出错
// 参数:无
// 返回:0:正常;1:空或出错
// 备注:如果队列头或队列尾相等,则有可能是队列没有消息
// 也有可能是消息过多而导致溢出,如果继续使用将导致信息丢失
// 所以要合理定义队列消息缓冲区的大小
bit QueueEmptyOrError(void)
{
return(MessageHead==MessageTail)?1:0;
}
/* ========== 定义广播消息处理机制 ========== */
// 功能:广播一条消息
// 参数:m,message类型,要广播的消息
// 返回:无
// 备注:这是由已有的代码做最小的修改而成的
#define Broadcast(m) PutMessage(m)
// 功能:以检听的方式获取第一条消息
// 参数:无
// 返回:message类型的消息
// 备注:这中取消息的机制不做安全检查,必须安全使用
#define AMessage() Messages[MessageHead]
// 功能:删除第一条消息
// 参数:无
// 返回:无
// 备注:在广播消息被听完后使用
#define DelMessage() GetMessage()
/* ========== 俱乐部的定义 ========== */
// 功能:娱乐俱乐部事务处理
// 参数:m,message类型,要检读的消息
// 返回:无
// 备注:
void Club_Recreation(message m)
{
switch(m)
{
caseMSG_SING:
// 唱歌:私有消息
break;
caseMSG_DANCE:
// 跳舞:私有消息
break;
caseMSG_PLAYTHEPIANO:
// 弹钢琴:私有消息
break;
caseMSG_GOTOGAME:
// 参赛:广泛消息
break;
caseMSG_ATTENDMEETING:
// 参加会议:广泛消息
break;
}
}
// 功能:体育俱乐部事务处理
// 参数:m,message类型,要检读的消息
// 返回:无
// 备注:
void Club_Sport(message m)
{
switch(m)
{
caseMSG_PLAYFOOTBALL:
// 踢足球:私有消息
break;
caseMSG_PLAYBASKETBALL:
// 打篮球:私有消息
break;
caseMSG_GOTOGAME:
// 参赛:广泛消息
break;
caseMSG_ATTENDMEETING:
// 参加会议:广泛消息
break;
}
}
main(void)
{
unsignedRandTaskGene;
while(1)
{
RandTaskGene = rand();
RandTaskGene %= 2;
if(RandTaskGene)
{
Broadcast(MSG_SING); // 产生消息
}
else
{
Broadcast(MSG_GOTOGAME); // 产生消息
}
RandTaskGene = rand();
if(RandTaskGene>30000)
{
Broadcast(MSG_PLAYBASKETBALL); // 产生消息
}
else
{
Broadcast(MSG_ATTENDMEETING); // 产生消息
}
while(!QueueEmptyOrError()) // 一次性消息检读
{
Club_Recreation(AMessage()); // 娱乐俱乐部检读消息
Club_Sport(AMessage()); // 体育俱乐部检读消息
DelMessage(); // 检读完毕,删除消息
}
}
}
3.11.3 回顾与思考3.11.2节的程序分别处理了私有消息和广播消息,消息的处理机制统一使用广播消息的处理机制。广播消息由Broadcast()来实现,它所要做的事情其实就是将消息送入消息队列。广播消息在发送上其实与普通的私有消息并没有什么区别,它们的区别在于检读者上。由于广播消息与私有消息没有区分标记,所以一个消息进入消息队列后,它们的身份无法辨认。无论使用Broadcast()来发送的消息,还是使用PutMessage()来发送的消息,从Broadcast()的定义可以看出,它们完全是一个东西,只不过叫法不同。
为了支持这种消息发送机制,消息检读工具AMessage()只是从队列的头部读出一条消息的内容,但是并不会让这条消息出队,这样在Club_Recreation()检读了这条消息后,Club_Sport()还有机会通过AMessage()来检读这条消息。到这里我们应该能明白,一条消息是广播消息还是私有消息的区别在哪里。
如果一条消息能被多个检读者识别,那么这条消息就是一条广播消息,而如果一条消息只能被某一个检读者识别,那么它就是一条私有消息。
也就是说,上面的消息处理机制把广播消息的处理与私有消息的处理完全采用了相同的处理机制,这显然是一种很好的机制。
当一条消息被所有的检读者都过目以后,它也就完成了自己的历史使命,最后就由DelMessage()来把它从队列中删除,为处理队列中的下一条消息提供机会。
1.疑问也许有人会说,这种处理方法似乎太过于形式化,把问题搞得神秘兮兮,我们为什么不按下面的方式来编程呢?
main(void)
{
unsigned RandTaskGene;
messagem; //加入的语句
while(1)
{
RandTaskGene= rand();
RandTaskGene%= 2;
if(RandTaskGene)
{
Broadcast(MSG_SING); //产生消息
}
else
{
Broadcast(MSG_GOTOGAME); // 产生消息
}
RandTaskGene = rand();
if(RandTaskGene>30000)
{
Broadcast(MSG_PLAYBASKETBALL); // 产生消息
}
else
{
Broadcast(MSG_ATTENDMEETING); // 产生消息
}
while(!QueueEmptyOrError()) // 一次性消息检读
{
m = GetMessage(); // 一次性取消息并删除该消息(加入的语句)
Club_Recreation(m); // 娱乐俱乐部检读消息
Club_Sport(m); // 体育俱乐部检读消息
DelMessage(); // 检读完毕,删除消息(减去的语句)
}
}
}
2.解答3.11.2节中的代码的编程风格是为了遵守概念约定,把消息广播与消息发送严格分开,上面的代码则破坏了这个概念约定,把消息发送与消息广播混杂在一起。
我们知道,单片机程序从操作系统管理代码到自身的代码都是自己编写的,适当地破坏点约定也许无伤大雅,取舍完全在编程魔法师的一念之间,只要能控制好局面就可以了,这里并没有十分严格的规定。