850 likes | 1.04k Views
活动对象( Active Object ). 活动对象. 活动对象被用于事件驱动的多任务处理 它们是 Symbian OS 的一个基本部分 本讲要讲解为什么活动对象非常重要 解释它们是如何怎样设计以进行响应式、高效率的处理事件. 活动对象. Symbian OS 中的事件驱动多任务处理 理解同步请求和异步请求的区别之处,并且能够对它们的典型进行区分 认识活动对象的典型应用 —— 处理异步任务并且不阻塞线程时被请求 理解用多线程和多活动对象实现多任务的区别,以及为什么后者在 Symbian OS 编码中要优先使用. Symbian OS 中的事件驱动多任务处理.
E N D
活动对象 • 活动对象被用于事件驱动的多任务处理 • 它们是 Symbian OS的一个基本部分 • 本讲要讲解为什么活动对象非常重要 • 解释它们是如何怎样设计以进行响应式、高效率的处理事件
活动对象 Symbian OS中的事件驱动多任务处理 • 理解同步请求和异步请求的区别之处,并且能够对它们的典型进行区分 • 认识活动对象的典型应用——处理异步任务并且不阻塞线程时被请求 • 理解用多线程和多活动对象实现多任务的区别,以及为什么后者在Symbian OS 编码中要优先使用
Symbian OS 中的事件驱动多任务处理 • 同步和异步请求 • 当程序的函数代码中要调用一个服务请求时,这个服务不是同步执行就是异步执行 • 同步函数 • 执行一个服务直到完成,然后返回其调用者,通常返回的是一个成功或者失败指示 • 异步函数 • 提交一个请求作为函数调用的一部分,并且马上返回到其调用者继续执行 • 在一段时间之后请求再完成
Symbian OS 中的多任务处理 • 调用一个异步请求以后 • 调用者可以自由处理其他问题或者简单的等待,后者就是常说的“阻塞” • 一旦服务完成,调用者可以接收到一个信号,显示请求是否成功 • 这个信号就是一个事件 • 这段代码被称为事件驱动 • 定时器等待是一个典型的异步调用的例子 • 另一个例子是Read()方法,它在Symbian OS的RSocket类中,这个方法是从远程主机收取数据
Symbian OS 中的线程 • 线程被内核以抢占的方式调度 • 内核执行已经就绪的最高优先级的线程 • 每个线程都可能由于等待一个事件的发生而暂停执行,而在适当的条件下恢复执行 • 内核控制线程的调度 • 允许线程通过时间片分割的方式共享系统资源,如果有更高级的线程处于就绪态,则抢先执行这个高优先级的线程 • 这种总是运行已就绪的最高优先级线程的方式是占先式多任务处理的基础
Symbian OS中的线程 • 当当前线程挂起时会发生上下文切换 • 上下文切换导致了运行时内核调度器的负载 • 如果原来的和替换的线程在不同的进程执行,会因为要交换进程内存以及写入缓存而带来更大开销 • 比线程上下文切换要慢100倍
事件驱动多任务处理 • 异步事件可发生在如下情况: • 从外部资源——例如用户输入或者硬件外设接收到数据。 • 通过软件——例如由定时器或者完成异步请求时引起。
事件驱动多任务处理 • 事件由事件处理器管理 • 事件处理器等待事件,然后处理它 • 一个高层的事件处理器的应用是网页浏览器应用 • 等待用户输入和响应,提交请求以接收Web页,并把接收到的页面显示出来 • 网页浏览器使用系统服务器,用以接收来自他客户端的请求。系统服务器接收到请求,并且继续等待另一个请求。在服务请求中,系统服务器依次将请求地交给其他的服务器,它们将稍后产生完成事件。 • 这里描述的每个软件组建都是事件驱动的。它需要对用户输入或者来自系统的请求作出响应 • 这样的话很快就会变得很复杂!
Symbian OS 中事件处理考虑 • 在响应一个事件的时候,事件处理器可能要请求另一个服务,而这将引发另一个事件(依次类推) • 操作系统必须有一个高效的事件处理模型,从而在事件发生后通过最合适的顺序尽快处理他们 • 对用户驱动的事件进行快速处理及给出响应以提供良好的用户体验是尤为重要的
Symbian OS 中事件处理考虑 • 要避免在事件之间持续的检测 • 这样不断的检测会导致大量的功耗,这在电池供能的设备上必须避免 • 操作系统在低能耗状态进行等待 • 在等待下一个事件的时候,软件应允许操作系统转入空闲模式 • 事件处理应使内存资源使用最小化 • 处理器资源也要有效的使用 • 活动对象满足这些要求,并提供了一种轻量级的事件驱动多任务处理机制
活动对象和活动调度器 • 活动对象和活动调度器(Active Scheduler) • 一起被称为“活动对象框架” • 用于简化异步程序设计,使编写代码更容易: • 提交异步请求 • 管理请求的完成事件 • 对结果的处理 • 通常,一个Symbian OS应用程序或服务包含单个主事件处理线程和与其相关的活动调度器 • 在线程中运行一组活动对象 • 活动对象有的事件处理方法以供活动调度器调用 • 每个活动对象封装了一个任务 • 它向其服务器提供者请求异步服务,并由活动调度器调用它处理请求完成事件
活动对象和活动调度器 • 活动对象框架被用于调度 • 同一个线程中多个异步任务的处理 • 所有活动对象都在同一个线程中运行,因此在活动对象中切换比线程间上下文切换的代价要低 • 这就使得活动对象通常是Symbian OS 中最合适的事件驱动多任务处理机制 • 活动对象在运行中是彼此独立的 • 就像在进程中线程的运行时彼此独立的一样 • 然而,由于在在同一线程中,活动对象更容易共享内存
活动对象和活动调度器 • 活动对象框架 • 是协作式或者非抢先的多任务处理 • 在线程中其他活动对象能够开始执行操作之前,每个活动对象的方法都会运行直到完成 • 当活动对象在处理事件时 • 它不能被线程中其他运行的活动对象所抢占 • 注意线程本身的调度是抢先式的(见前面的幻灯片)
活动对象和活动调度器 • 一个Win32 应用(例如在Windows下运行的程序) 使用了一个简单的消息循环和消息分发的模式 • 在 Symbian OS 中,活动调度器代替了Windows中的消息循环,活动对象的事件处理函数相当于消息处理函数 • 由活动调度器执行的事件完成处理与事件引发的特定动作是解耦的——它们由单个活动对象执行 • 例如:email 发送完成事件——其处理动作就移开了”发送”对话框
实时性考虑说明 • 有些事件要求在有保证的时间内响应 • 这被称为“实时”事件处理 • 例如一个实时任务可能被要求保持为一个声音驱动的缓冲区提供声音数据,相应延迟就会导致声音解码的延迟,其结果就是声音的中断 • 另一个典型的实时要求可能更严格,例如底层的电话通信 • 不同任务对实时响应有不同的需求 • 这些可以用任务优先级表示 • 高优先级任务总必须能抢占低优先级任务,从而保证符合他们的实时性要求 • 任务要求的响应越短,它应赋予的优先级越高
活动对象不适合实时任务 • 当一个活动对象在处理事件时,它无法被同一线程的另一个活动对象的事件处理程序所抢占 • 因此,他们不适合实时任务 在 Symbian OS中, 实时任务应当用高优先级线程和进程来实现,并根据相对的实时性要求选择适当的优先级
活动对象 CActive类 • 了解活动对象优先级别的重要性 • 知道活动对象事件处理方法 (RunL()) 是非抢占的 • 知道活动对象的继承特性,以及需要实现和覆盖的函数 • 了解如何构建、使用和销毁活动对象
活动对象 • 引言 • 每个活动对象请求一个异步服务,并且在请求过后一段时间处理完成事件 • 它也提供一种撤销未完成请求的方法,还有异常情况下的错误处理 • 一个活动对象类必须派生自CActive类,该类定义在e32base.h中
活动对象类的构造 • CActive类是一个抽象类,拥有两个纯虚函数 • RunL()和 DoCancel()–所有具体的活动对象类必须从CActive类中继承,定义和实现这些方法 • 它还拥有一个TRequestStatus成员变量用以接收异步请求的完成结果 • 在构造时 • 从CActive派生的类必须调用基类中的保护型构造函数 • 同时传递一个参数来设置活动对象的优先级 • 和线程类似,所有的活动对象都有一个优先级值来决定它们被如何调度
为什么有活动对象优先级? • 当活动对象关联的异步服务完成时会产生一个事件,活动调度器能侦测到这个事件. • 活动调度器决定每个事件关联的是哪个活动对象 • 然后调用恰当的活动对象去处理事件 • 当活动对象在处理事件时 • 直到事件处理函数返回到活动调度器,它是无法被抢占的 • 因此很可能很多事件在控制返回到活动调度器之前已经完成……
为什么有活动对象优先级? • 要解决哪个活动对象是下一个要运行的 • 活动调度器通过活动对象的优先级进行排序,而不是根据完成的顺序 • 否则,一个具有低优先级的事件,如果他在一个更重要的事件之前完成,那么它就会将高优先级事件的处理推迟一段时间,而这段时间的长度是不确定的 • 优先级只是用来决定事件处理者运行的顺序的 • 如果一个高优先级活动对象收到一个事件处理请求,而此时一个低优先级的活动对象正处理事件,这个低优先级的事件处理器是不能被抢占的
优先级 • 定义了一系列的优先级 • 在CActive类中的TPriority枚举类 • 通常使用优先级 CActive::EPriorityStandard (=0),除非有很好的理由不去这么做
活动对象类的构造 • 作为构造的一额外部分,活动对象代码应当调用活动调度器的静态函数CActiveScheduler::Add() • 这可以将活动对象加入到那个线程中的事件处理活动对象列表中 • 列表由活动调度器来维护 • 列表用活动对象的优先级排序,最高优先级的活动对象在前面
活动对象类的构造 • 活动对象典型的拥有的一个对象的句柄 • 向它发出异步完成的请求,例如RTimer类型的定时器对象 • 这个对象通常被认为是异步服务提供者,它可能需要作为活动对象构造的一部分被初始化 • 如果初始化失败,它必须在第二阶段构造中执行
提交异步服务请求 • 活动对象类 • 会向调用者提供公有的请求发起方法以初始化请求 • 这些方法通过建立好的模式向活动对象对应的异步服务者提交请求 • 过段时间请求完成,产生一个事件 • 如下所示...
提交异步服务请求 • 1. 检查以前的未完成请求 • 请求方法应该检查在试图提交另一个请求之前没有请求已被提交了. • 每一个活动对象只能有一个已未完成的请求. 依赖于实现,代码可能有如下几种情况: • 如果请求已经发出,就发生致命系统 (这种情况只会因为程序设计错误而发生) • 如果尝试提交多个请求是合法饿,拒绝提交另一个请求, • 取消先前的请求并提交一个新的请求
提交异步服务请求 • 2. 请求的结果 • 活动对象应该向服务提供者发送请求,传入其 TRequestStatus&类型的iStatus成员变量作为参数 • 服务提供者在开始异步请求之前要把这个值赋给 KRequestPending
提交异步服务请求 • 3. 调用SetActive()方法标识对象为“等待”状态 • 如果请求被成功的提交了,请求方法将调用CActive 基类中的SetActive()方法 • 从而向活动调度器指明请求是已经被提交并且是未完成的 • 只有在请求已经提交了以后,才能调用这个函数
事件处理 • 每个活动对象类 • 必须实现 CActive 基类的纯虚成员函数 RunL() • 这是当来自异步服务提供者的完成事件发生时调用的事件处理函数 • 活动调度器选择了一个活动对象处理该事件时,就调用这个函数 • RunL()方法 • 这函数的名字有一定的误导性,因为异步函数已经在运行了 • 或许 HandleEventL()或者 HandleCompletionL()这样的名字会更清楚的描述其功能
事件处理 • RunL()的典型行为 • 通过监测活动对象的TRequestStatus 对象中的iStatus即一个32位整形值,来判定异步请求是否成功完成 • RunL()通常或者发送另一个请求或者告知系统中其他对象事件完成 • RunL()的代码复杂程度可能变化很大 • 一旦RunL()开始执行 • 它无法被其他活动对象的事件处理器所抢占 • 正是由于这个原因代码应该尽快完成,这样其他事件才能够无延误的处理
活动对象 异步服务提供者 1. 提交请求并传递iStatus 2. 设置 iStatus=KRequestPending并开始服务 3. 对自身调用SetActive() 进行服务处理 4. 服务完成,服务提供者通过 RequestComplete()来通知活动调度器,并传递一个完成结果 活动规划器 5. 活动调度器对活动对象调用 RunL()来处理该事件 6. RunL()重新提交另一个请求,并且不可被抢占 进程或线程边界 事件处理 • 此图展示了当前活动对象向一个异步服务提供者提交一个请求,一段时间后请求完成了,产生一个事件,而事件被 RunL()处理时基本的动作执行序列
撤销未完成的异步请求 • 一个活动对象 • 必须能够撤销任何未完成的、自己所产生的非同步请求 • 例如,活动对象所在的应用程序线程将要终止,他必须能够阻止请求 • CActive基类 • 实现了一个 Cancel()方法,该方法调用纯虚方法DoCancel()然后等待请求尽早完成 • 所有的DoCancel()实现都应该对异步服务提供者调用恰当的撤销函数
撤销选定的异步请求 • DoCancel()也可以包括其他处理 • 但它不应该异常退出或分配资源,而且也不应该执行长时间的操作 • 限制该方法只执行撤销工作以及与与撤销关联的需要清除工作,而非实现复杂的功能,是一个很好的准则 • 这是因为析构函数应该调用Cancel(),而可能已经清除了DoCancel()函数所需要的资源 • 不需要检查 ... • 在调用 Cancel()之前,请求是否已经被被选中的 • 这样做也是安全的,即便它当前不是活动的,例如正在等待事件
错误处理 • 从Symbian OS v6.0 之后 • 活动对象提供了 RunError()虚方法,当活动对象的RunL()方法中发生异常退出的时候,活动调度器会调用这个方法 • 该方法接受一个异常退出码作为参数,返回一个错误码用以表明异常退出是否已经被处理了 • 缺省的实现是不处理异常退出,而只是简单的返回传入函数的异常退出码 • 如果活动对象能够处理RunL()中发生的所有异常退出 • 它应该通过覆写缺省的实现CActive::RunError()来处理错误然后返回KErrNone • 如果在RunL()中不会发生异常退出,则不必覆盖
错误处理 • 如果 RunError()返回非KErrNone的值,表明异常退出尚未解决 • 那么活动调度器会调用它自己的Error()函数来进行处理 • 活动调度器 • 没有任何可用于执行错误处理的关于活动对象的上下文信息 • 因此,通常更倾向于在相应活动对象的 RunError()方法中进行错误恢复
析构活动对象 • CActive派生类的析构函数应该总是调用Cancel() • 作为清除代码的一部分来终止选定的请求 • 理想的情况是在活动对象持有的其他资源被销毁之前完成该调用,因为这些资源可能会被服务提供者或者DoCancel()方法所使用 • 析构代码 • 应该释放对象持有的所有资源,包括所有异步服务提供者的句柄
析构活动对象 • 基类CActive的析构函数是虚函数 • 对对象进行检查,确保活动对象当前不是活动的 • 如果有任何请求被选定,即 Cancel()没有被调用,那么将会发生致命错误 • 该致命错误捕获所有导致它的程序设计错误 • 如一个请求在处理它的活动对象被销毁后完成了 • 这会导致一个“迷失信号” (“stray signal”),这时活动调度器无法找到一个活动对象来处理事件 • 再验证确认活动对象没有已提交的请求被选定后 • CActive析构函数会将活动对象从活动调度器中移除
活动对象类的示例 • 下面的例子 • 描述了如何用活动对象包装异步服务 • 这个例子中采用的是RTimer服务提供的一个定时器 • Symbian OS 已经提供了一个抽象活动对象类 CTimer该类包装了RTimer,并且可以继承该类 • 但是,这里还是使用本例,因为它用简单明了的方式展示了如何写一个活动对象类 • 下面的幻灯片 • 展示了涉及到的类以及它们和活动调度器的关系
CActive CExampleTimer CActiveScheduler RTimer iActive iStatus n 1 NewL() After() RunL() DoCancel() RunError() Add() ... RunL() DoCancel() RunError() CExampleTimer及其与 RTimer, CActive和 CActiveScheduler之间的关系 iTimer 活动对象类的示例
CExampleTimer 类 class CExampleTimer : public CActive { public: ~CExampleTimer(); static CExampleTimer* NewL(); void After(TTimeIntervalMicroSeconds32& aInterval); protected: CExampleTimer(); void ConstructL(); // 两阶段构造 protected: virtual void RunL(); // 从CActive继承 virtual void DoCancel(); virtual TInt RunError(TInt aError); private: RTimer iTimer; TTimeIntervalMicroSeconds32 iInterval; };
CExampleTimer 构造函数 CExampleTimer::CExampleTimer() : CActive(EPriorityStandard) { CActiveScheduler::Add(this); } void CExampleTimer::ConstructL() { User::LeaveIfError(iTimer.CreateLocal()); } CExampleTimer* CExampleTimer::NewL() {...} CExampleTimer::~CExampleTimer() { Cancel(); iTimer.Close(); // 关闭句柄 } 创建异步服务提供者 第二阶段构造,出于整洁,略去代码
CExampleTimer::After() void CExampleTimer::After(TTimeIntervalMicroSeconds32& aInterval) { if (IsActive()) { _LIT(KExampleTimerPanic, "CExampleTimer"); User::Panic(KExampleTimerPanic, KErrInUse)); } iInterval = aInterval; iTimer.After(iStatus, aInterval); SetActive(); } 同时只允许提交一个定时器请求,调用者在提交另一个请求之前必须调用Cancel() 设置 RTimer 将该对象标志为活动的
RunL()和 DoCancel() void CExampleTimer::RunL() { User::LeaveIfError(iStatus.Int()); _LIT(KTimerExpired, "Timer Expired\n"); RDebug::Print(KTimerExpired); iTimer.After(iStatus, iInterval); SetActive(); } void CExampleTimer::DoCancel() { iTimer.Cancel(); } 事件处理方法 如果发生错误就退出并且在 RunError()中处理错误 否则, 标记完成时间 提交计时请求 撤销定时器
错误处理和事件处理 • 如果没有发生错误 • 事件处理者RunL()使用RDebug::Print()记录定时器完成情况作为调试输出 • RunL()用存储的间隔值重新提交计时器请求 • 一旦定时器请求已经开始,它将持续运行并且不断被提交 • 直到活动对象调用Cancel()方法将其停止
错误处理和事件处理 • 事件处理者RunL()负责检查活动对象的iStatus结果 • 如果 iStatus的值不是KErrNone就异常退出,这样RunError()方法可以处理发生的问题 • 在这种情况下 –错误处理十分简单: • 从请求返回的错误被记录作为调试输出 • 这可能在RunL()方法中已经完成 • 但是这被分离到了RunError()方法中,用来演示如何使用活动对象框架将错误处理从时间处理的主逻辑中分离出来
CExampleTimer::RunError TInt CExampleTimer::RunError(TInt aError) { _LIT(KErrorLog, "Timer error %d"); RDebug::Print(KErrorLog, aError); return (KErrNone); } 该函数在 RunL()发生异常退出时被调用(aError包含一个异常退出码) 记录错误已被处理
活动对象 活动调度器 • 了解活动调度器的角色和属性 • 知道 CActiveScheduler::Start()只能在至少有一个活动对象请求发出的时候之后才能被调用 • 明白线程处理事件失败的典型的原因是可能是活动调度器没有启动或者过早的停止了 • 理解 CActiveScheduler可以称作继承,以及创建一个派生活动调度器的原因
建立和安装活动调度器 • 绝大多数线程都有活动调度器 • 它通常是由框架隐式创建的 • (例如 GUI 框架中的CONE) • 服务器代码必须在使用活动对象之前显示创建并启动一个活动调度器 • 基于控制台的测试代码如果依赖了一些使用活动对象的组件,必须在其主线程中创建一个活动调度器
建立和安装活动调度器 • 建立和安装活动调度器的代码: CActiveScheduler* scheduler = new(ELeave) CActiveScheduler; CleanupStack::PushL(scheduler); CActiveScheduler::Install(scheduler);